Repository navigation
Refactor ClientStateVar: scoped client state via a useClientState hook (rx.client_state) #7610
Copy link
Copy link
Open
Open
Feature
Copy link
Labels
roadmaphttps://github.com/reflex-dev/reflex/issues/2727https://github.com/reflex-dev/reflex/issues/2727
Description
Problem
rx._x.client_state (ClientStateVar) is experimental and implemented as a per-component useState plus a shared mirror in refs, fanned out through the app root. Known limitations:
- Everything is global. There is no per-component or per-
rx.foreach-item client state without inventing unique names; the same var used in two@rx.memoinstances shares one value. - Over-rendering. A write to one var re-renders every component that uses any client state.
- Scope bugs. A setter declared in one component is not reachable from a sibling (Local rx._x.client_state setter raises ReferenceError when the setter is on a sibling component #7426); a component that mounts while the value is non-default never re-renders on the push back to the default (ClientStateVar: a component that mounts while the value is non-default never re-renders on the push back to the default #6823); invalid JS identifiers produce broken hook code (ClientStateVar accepts invalid JavaScript identifiers and emits broken hook code #6828).
- SSR leakage. Module-level stores are shared between server-rendered requests.
- Loop vars go out of scope.
rx.foreachitem and index compile to the.mapcallback's parameters, which are unreachable once a consumer is lifted into its own function; anon_submithoisted into auseCallbackthrowsReferenceError: index is not defined([REF-2745] rx.form handle_submit lambda cannot reference foreach vars #3210).
Proposal
Reimplement client state on a useClientState hook backed by a per-var slot store, and promote the API to rx.client_state(default, name=...).
- Scoping. Named vars are global and addressable from the backend; unnamed vars are scoped to the component tree that first uses them, so each
@rx.memoinstance and eachrx.foreachitem gets its own value, the way React'suseStatedoes. AClientStateProviderapp wrap mounts the store;ClientStateScopemarks scope boundaries (one per rendered loop item). - Per-slot subscriptions. Each var has its own listener set, so writing one var only re-renders its subscribers.
- Backend integration. Global vars can be pushed from and retrieved by event handlers over new wire events (
_client_state_set,_client_state_get). - Functional updaters.
.setaccepts a value, aFunctionVar, or a Python lambda traced at compile time (count.set(lambda v: v + 1)) and replaces.set_value. - SSR isolation. A fresh store per server-rendered request.
- Compiler. Client-state setters are auto-memoized; hooks, imports and app wraps are collected through
VarData. Each renderedforeachitem publishes its value and index to its subtree, so a consumer reads them wherever the compiler places it (fixes [REF-2745] rx.form handle_submit lambda cannot reference foreach vars #3210).Foreachis no longer a snapshot boundary for auto-memoization, so a loop-var consumer becomes its own memo component. - Compat.
rx._x.client_state.ClientStateVarkeeps working with a deprecation warning;rx.client_stateis the stable path. - Coverage. Vitest tests for the store, subscriptions, SSR isolation, provider lifecycle and scopes (Unit-test the shipped frontend JavaScript with standard JS tooling (vitest) #7611); Python unit tests for hook/import/app-wrap emission and setters; Playwright tests for runtime behavior, backend push/retrieve, and per-item loop state.
Pending work
The implementation is #6936 (draft, 48 files, +6664/-533). Outstanding before it can be marked ready:
- Compile-time cost on
foreach-heavy pages. CodSpeed reports roughly 5-6% on the_stateful_pagecompile and evaluate benchmarks, because each loop-var consumer now gets its own memo definition and each loop var carriesVarData. Half of the original regression was recovered in 70b34c5 by dropping a redundant evaluation increate_passthrough_component_memo; the remainder is inherent to the fix. Needs either an explicit acknowledgement on CodSpeed or a focused follow-up on the memo tag hash (_compute_memo_tag/_update_deterministic_hash), which is the dominant remaining cost. - Rebase onto current
main(base is 8239259) and reconcile with the in-flight fixes on the legacy implementation: Fix local client state across sibling components #7442 (sibling setters, Local rx._x.client_state setter raises ReferenceError when the setter is on a sibling component #7426) and fix: validate client state JavaScript identifiers #7595 (identifier validation, ClientStateVar accepts invalid JavaScript identifiers and emits broken hook code #6828). - Confirm ClientStateVar: a component that mounts while the value is non-default never re-renders on the push back to the default #6823 is covered by the new store (a late-mounting subscriber must read the live slot value rather than a seeded default) and add a regression test if not.
- Final review and un-draft.
Related
- Local rx._x.client_state setter raises ReferenceError when the setter is on a sibling component #7426, ClientStateVar: a component that mounts while the value is non-default never re-renders on the push back to the default #6823, ClientStateVar accepts invalid JavaScript identifiers and emits broken hook code #6828, [REF-2745] rx.form handle_submit lambda cannot reference foreach vars #3210
- Unit-test the shipped frontend JavaScript with standard JS tooling (vitest) #7611 JS unit tests for the shipped frontend
Metadata
Metadata
Assignees
Labels
roadmaphttps://github.com/reflex-dev/reflex/issues/2727https://github.com/reflex-dev/reflex/issues/2727
Activity
linear-code commentedon Oct 20, 2026
ENG-13449