Promote client state to rx.client_state with a scoped store
Merged
MA
3d ago+6422+6422+0123456789012345678901234567890123456789-707-707-012345678901234567890123456789
Type of change
- New feature (non-breaking change which adds functionality)
- This change requires a documentation update
Description
Promote client-only state to rx.client_state with a store-backed React runtime.
What changes for users
rx.client_state(default, name=...)is the public API. A named var is global: any component can read and write it, and the backend canpushandretrieveit. An unnamed var is owned by the outermost page or@rx.memobody that uses it, so each@rx.memoinstance owns its own value, the way React'suseStatedoes, and a page's unnamed vars reset when the page unmounts.- Writes are per var: setting one var re-renders only the components that read it.
.settakes a value, aFunctionVar, or a lambda traced at compile time (count.set(lambda v: v + 1)), and replaces.set_value.rx._x.client_stateandClientStateVar.create(var_name, default, global_ref)keep working with a deprecation warning. An unnamed legacy var stays global unlessglobal_ref=Falsewas passed.- Server-side rendering gets a fresh store per request.
Implementation
reflex_base.client_state:ClientStateVar,ClientStateSetter, theclient_state()factory..templates/web/utils/client_state.js: slots with independent subscriptions, a scope chain that resolves a name to the slot its owning scope holds, andgetClientState/setClientStatefor code outside the React tree.- The owner of an unnamed var is decided at compile time. Each page and each
@rx.memobody that uses client state is wrapped in aClientStateScopethat lists the unnamed names its body uses (owns=[...]);ClientStateScopePluginbuilds a page's list from the compile walk, including bodies the memoize plugin moved into their own modules. A scope claims each listed name before its descendants render unless an ancestor owns it, so the owner never depends on render order. Auto-memoize wrappers stay invisible to scoping. ClientStateProviderapp wrap provides the root scope;state.jspublishes the store onrefs["__client_state"]for backend events andrx.call_scriptcode.- New
tests/js/vitest harness for the shipped frontend JavaScript, wired into the unit-tests workflow.
Not in this PR
Per-item scoping of rx.foreach loop vars (providing the item and index to handlers and memoized descendants, and isolating each item in its own component) moves to a follow-up PR. Until it lands, per-item client state inside a loop is written with an @rx.memo item function, which the docs show.
Checklist
- Tests pass with adequate coverage (unit, JS, Playwright)
-
uv run ruff check .anduv run ruff format .clean -
uv run pyright reflex testspasses -
pyi_hashes.jsonupdated - Documentation updated
- News fragments added
- Deprecation warnings added for the old API
https://claude.ai/code/session_017DE4XVgDNtq2i89CMVspJ9
Generated by Claude Code