Skip to content

feat: let the app handle a state schema mismatch - #7653

Open
benedikt-bartscher wants to merge 2 commits into
reflex-dev:mainfrom
benedikt-bartscher:state-mismatch-event
Open

benedikt-bartscher wants to merge 2 commits into
reflex-dev:mainfrom
benedikt-bartscher:state-mismatch-event

Conversation

@benedikt-bartscher

Copy link
Copy Markdown
Contributor

Since #7553, a production page that connects to a backend with another state schema reloads at once, or shows the "new version available" banner when it already reloaded inside REFLEX_AUTO_RELOAD_COOLDOWN. On a rolling deploy every open tab reconnects to a new pod within seconds, so a deploy that adds a var or renames an event handler reloads all of them in the middle of whatever the user is doing: a drag, a drawing or input not yet sent is lost. The app knows better when a reload is harmless, but it has no way to make that call; the only lever is the cooldown, which the reload on frontend errors shares.

Before it reloads or shows the banner, handleStateMismatch now dispatches a cancelable reflex:state-mismatch event on window, with the state schema hashes of both sides:

new CustomEvent("reflex:state-mismatch", {
  cancelable: true,
  detail: { frontend: app.stateSchema, backend: info.backend },
});

A listener that calls preventDefault() skips both the reload and the banner, so the app can show its own notice, reload after an autosave or when the user pauses, or do nothing. This is the second option from the issue: it is frontend-only, adds no config, and covers the "reload", "banner" and "none" policies of the first option as well as anything in between. Detection, the hash, the cooldown and the banner are unchanged. Without a listener, or with one that does not cancel the event, a page behaves exactly as before.

In dev the event is not dispatched. That path only warns, because a backend hot reload can reconnect before Vite serves the rebuilt bundle; a listener that reloads would turn that short-lived mismatch into reloads during development.

app = rx.App(
    head_components=[
        rx.script(
            """
            window.addEventListener("reflex:state-mismatch", (event) => {
                event.preventDefault();
                // Show your own notice, and reload when it is safe.
            });
            """
        ),
    ],
)

The event comes with every mismatched connect and reconnect, also after a reload the app started itself. The cooldown only guards the reload Reflex does on its own, which docs/utility_methods/exception_handlers.md now points out next to the example.

Tests:

  • tests/units/reflex_base/client_state_updates.mjs runs the real state.js: the event is dispatched before the reload and carries both hashes; canceling it skips the reload and the banner (with and without a recent reload), leaves the cooldown untouched and lets events through; a dev page does not dispatch it. The two prod tests fail without the change.
  • tests/integration/tests_playwright/test_client_error.py gains a /handled page whose rx.script cancels the event. In prod the page stays on its first navigation, shows no banner and still handles events; in dev it only warns. The extra backend state the mismatch tests add moved into a fixture both tests share. The prod case fails without the change.

uv run pytest tests/units and uv run pytest tests/integration/tests_playwright/test_client_error.py (dev and prod) pass, and the pre-commit hooks are clean. I also checked the documented setup, an rx.script in head_components, against a dev and a prod build.

Closes #7563

🤖 Generated with Claude Code

Before a production page reloads or shows the "new version available"
banner for a state schema mismatch, it now dispatches a cancelable
`reflex:state-mismatch` event on `window`, with the frontend and backend
state schema hashes in `event.detail`. A listener that calls
preventDefault() skips both, so the app can defer the reload to a safe
moment or show its own notice instead. Without a listener nothing
changes, and dev mode still only warns without dispatching the event.

Closes reflex-dev#7563

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no actionable issues were found.

Summary

Lets production apps cancel the default reload or banner when frontend and backend state schemas differ.

  • Apps can choose how production pages respond to state mismatches.

Reviews (2) · Last reviewed commit: "test: drop the extra state's event handl..." · Reviewed by Greptile

@codspeed

codspeed Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

1 warning

⚠️ 17 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

✅ 148 untouched benchmarks


Comparing benedikt-bartscher:state-mismatch-event (0efcd7e) with main (656e8c7)

Open in CodSpeed

@benedikt-bartscher
benedikt-bartscher marked this pull request as ready for review October 25, 2026 15:45
@benedikt-bartscher
benedikt-bartscher requested review from a team and Alek99 as code owners October 25, 2026 15:45

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 5 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="docs/utility_methods/exception_handlers.md">

<violation number="1" location="docs/utility_methods/exception_handlers.md:32">
P3: This example cannot run as shown because `rx` is undefined. Add `import reflex as rx` before constructing the app.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

Comment thread tests/integration/tests_playwright/test_client_error.py
An immediate reload can cost the user input not yet sent or an interaction in progress, for example when a rolling deploy reconnects every open tab to a new backend. Before it reloads or shows the banner, a production page dispatches a cancelable `reflex:state-mismatch` event on `window`, with the state schema hashes of the frontend and the backend in `event.detail.frontend` and `event.detail.backend`. A listener that calls `event.preventDefault()` skips both, and the app decides what to show and when to reload:

```python
app = rx.App(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This example cannot run as shown because rx is undefined. Add import reflex as rx before constructing the app.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/utility_methods/exception_handlers.md, line 32:

<comment>This example cannot run as shown because `rx` is undefined. Add `import reflex as rx` before constructing the app.</comment>

<file context>
@@ -26,6 +26,25 @@ On a mismatch:
+An immediate reload can cost the user input not yet sent or an interaction in progress, for example when a rolling deploy reconnects every open tab to a new backend. Before it reloads or shows the banner, a production page dispatches a cancelable `reflex:state-mismatch` event on `window`, with the state schema hashes of the frontend and the backend in `event.detail.frontend` and `event.detail.backend`. A listener that calls `event.preventDefault()` skips both, and the app decides what to show and when to reload:
+
+```python
+app = rx.App(
+    head_components=[
+        rx.script(
</file context>
Suggested change
app = rx.App(
import reflex as rx
app = rx.App(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this as is. A plain ```python block is display-only (only exec/demo/eval blocks run), and short rx.App(...) config snippets in the docs routinely start without the import, e.g. docs/styling/theming.md, docs/styling/custom-stylesheets.md and docs/styling/tailwind.md (21 such snippets). The snippet only shows where the listener goes.

The mismatch_reports fixture removed ClientErrorExtraState from the state
registries but left its generated handlers (setvar) in
RegistrationContext.event_handlers. Each definition gets a new dynamic
name, so the entries piled up for the rest of the harness's context.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Let the app choose what happens on a state schema mismatch, instead of an immediate reload

1 participant