Detect frontend/backend state tree mismatch on connect
Merged
FA
3d ago+898+898+012345678901234567890123456789-26-26-01234567890123456789
All Submissions:
- Have you followed the guidelines stated in CONTRIBUTING.md file?
- Have you checked to ensure there aren't any other open Pull Requests for the desired changed?
Type of change
- New feature (non-breaking change which adds functionality)
- This change requires a documentation update
New Feature Submission:
- Does your submission pass the tests?
- Have you linted your code locally prior to submission?
Changes To Core Features:
- Have you added an explanation of what your changes do and why you'd like us to include them?
- Have you written new tests for your core changes, as applicable?
- Have you successfully ran tests with your changes locally?
Description
This PR adds detection of frontend/backend state tree mismatches when the frontend connects to the backend. The compiled frontend and backend can come from different builds (e.g., a backend deploy while a browser keeps an old page, or two backend versions behind a load balancer during a rolling deploy). Reflex now detects this incompatibility and handles it gracefully.
Key Changes
Backend (reflex/state.py, reflex/app.py):
- Added
BaseState._to_frontend_schema()method that computes a stable hash of the state tree structure (state names, frontend vars with types, and event handler names). Defaults and values are excluded so trees are compatible when structurally equivalent. - The hash is cached and invalidated when any state in the tree is modified.
- On socket connect, the backend compares the frontend's schema hash (sent in the CONNECT auth payload) against its own. If they mismatch, it emits a
state_mismatchevent before hydration.
Frontend (packages/reflex-base/src/reflex_base/.templates/web/utils/state.js):
- The compiled context module now exports
stateSchema, the hash of the state tree the frontend was built for. - The frontend sends this hash in the socket.io CONNECT
authpayload under thestate_schemakey on every connect and reconnect. - Added
handleStateMismatch()to respond to the backend'sstate_mismatchevent:- In production: Reloads the page once per cooldown window (10 seconds). If a reload already happened within the cooldown, shows a "new version available" banner with a reload button instead, preventing reload loops when the same mismatch persists (e.g., cached bundle or multiple backend versions).
- In development: Only logs a warning, allowing hot reload to work without interruption.
Compiler (reflex/compiler/compiler.py):
- Updated
compile_contexts()to pass the state schema hash to the context template.
Constants (packages/reflex-base/src/reflex_base/constants/):
- Added
STATE_MISMATCHsocket event andCONNECT_AUTH_STATE_SCHEMAcompile variable.
Testing
-
Added comprehensive unit tests in
tests/units/test_state.py:test_frontend_schema_is_stable_across_processes: Verifies the hash is deterministic across Python interpreters.test_frontend_schema_ignores_defaults: Confirms defaults don't affect the hash.test_frontend_schema_changes_with_tree_vars_and_handlers: Tests that structural changes (renaming states/vars, adding/removing handlers) invalidate the hash.test_frontend_schema_changes_when_state_added: Verifies dynamic state registration invalidates the cache.test_frontend_schema_leaves_out_backend_vars: Confirms backend-only vars don't affect the hash.
-
Added unit tests in
tests/units/test_app.py:test_on_connect_reports_state_schema_mismatch: Verifies the backend detects and logs mismatches, emits the event, and sanitizes client-supplied hashes.