Fix ClientStateVar late mount default sync - #6824
Conversation
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
hey @adhami3310 can you go through this ? |
Greptile SummaryThis PR corrects late-mounted global
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains; the explicit undefined check preserves a shared null during late mounting, resolving the previously reported issue.
|
| Filename | Overview |
|---|---|
| reflex/experimental/client_state.py | Adds a lazy global-state initializer with an explicit undefined check, correctly preserving shared null values while leaving local initialization unchanged. |
| tests/units/test_client_state.py | Verifies generated hooks for shared-ref initialization, explicit null preservation, undefined fallback, and local-state isolation. |
| tests/integration/tests_playwright/test_client_state.py | Covers the late-mount synchronization flow and confirms both consumers update when shared state returns to its default. |
| news/6823.bugfix.md | Accurately documents the corrected late-mounted global client-state behavior. |
Reviews (4): Last reviewed commit: "Merge branch 'main' into fix/client-stat..." | Re-trigger Greptile
Merging this PR will not alter performance
Comparing Footnotes
|
|
can you add the repro as integration test |
|
Added the repro as a Playwright integration test in 45c088e. It follows the issue scenario: push the global |
|
i ended up refactoring client_state rather completely in #6936 which structurally addresses this and other problems we've seen with the current useState-based ClientStateVar. i'll port over the regression test from this branch into mine to ensure the case remains covered under the new implementation. |
…uage The `refs` key the provider publishes its store on was spelled out three independent times in source: the `CLIENT_STATE_REF` constant in `client_state.js`, two hardcoded literals in `state.js`, and the Python expression `client_state.py` emits. A partial rename would have disconnected the backend event handlers from the mounted provider. There are now exactly two definitions, one per language, because neither can import the other's: `CLIENT_STATE_REF` in `client_state.js` and `CLIENT_STATE_REF` in `reflex_base.constants.state`. `state.js` imports the frontend constant instead of respelling it -- the cycle its old comment warned about no longer exists, since the provider takes the registry as a prop rather than importing `refs`. A vitest case asserts the two constants are equal by reading the Python source, so a rename on either side fails loudly, and a second one keeps `state.js` from regressing to a hardcoded copy. Both directions verified by mutation. Also ports the late-mount regression test from #6824 (issue #6823): a consumer mounting after a value has been pushed reads the live value rather than seeding a copy from the default and then sitting stuck when the value returns to that default. This holds by construction here -- there is one slot per name and a late consumer binds to it -- and the test fails if slot claiming re-seeds. The unit tests from that PR are not ported: they assert hook strings from the per-component `useState` design this branch replaces. Adds changelog fragments, and skips lockfiles in codespell so the new `tests/js/package-lock.json` integrity hashes do not trip it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DE4XVgDNtq2i89CMVspJ9
Audited the registry for retained values and added the guards. No leak found, but the invariants that make that true were untested, and two of them are easy to break. What holds a slot: a scope's `owned` map. Scopes point *up* to their parent and a parent keeps no list of children, so an unmounted `ClientStateScope` -- one `rx.foreach` item, one `@rx.memo` instance -- takes its map and every slot in it out of reach. Verified a loop adds nothing to the root scope as its list churns, so the store cannot grow with the number of items ever rendered. Root-scope slots are the deliberate exception: they outlive their consumers, because a named var is app-wide and the backend can push to it with nothing mounted -- releasing on last unmount would undo the late-mount behavior ported from #6824. Their number is fixed at compile time by the named and page-level `rx.client_state` call sites, so the retention is bounded, not unbounded. That exception makes one thing load-bearing: a listener on a root slot that outlived its component would pin that component's React internals for the life of the page. Tested by wrapping the slot's subscribe and asserting the count returns to zero on unmount. The remaining behavior is React's, and it is worth stating: keys decide what a row's state belongs to. Under positional keys -- what `rx.foreach` emits by default -- changing the list re-renders rows in place rather than unmounting them, so a row's client state stays with the position, not the item. Keyed by identity the old rows unmount and their state is released. Both directions are now tested, and the docs say so, since a loop item could not hold state before this branch. All three invariants fail under mutation: a no-op unsubscribe, a `ScopedValues` that stops opening a scope, and an item scope that resolves to the root. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DE4XVgDNtq2i89CMVspJ9
Fixes #6823
Summary
Fixes a global
ClientStateVarrendering bug where a component that mounted while the shared value was non-defaultcould get stuck when the value was later pushed back to the default.
Changes
ClientStateVarlocal state from the sharedrefsmirror when available.useState(default)behavior for non-global client state.undefinedTesting
uv run pytest tests/units/test_client_state.py -quv run pytest tests/units/compiler/test_memoize_plugin.py -k client_state -quv run ruff check reflex/experimental/client_state.py tests/units/test_client_state.pyuv run ruff format --check reflex/experimental/client_state.py tests/units/test_client_state.pyuv run pyright reflex/experimental/client_state.py tests/units/test_client_state.pyuv run towncrier check --config pyproject.toml --dir . --compare-with origin/main