Detect frontend/backend state tree mismatch on connect

Merged
FA
3d ago
+898-26

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_mismatch event 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 auth payload under the state_schema key on every connect and reconnect.
  • Added handleStateMismatch() to respond to the backend's state_mismatch event:
    • 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_MISMATCH socket event and CONNECT_AUTH_STATE_SCHEMA compile 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.

https://claude.ai/code/session_01EYJS7NPEz1gNkXZdqQDTA3