Tighten the rx.client_state contract before 0.11.0
Merged
MA
3d ago+613+613+012345678901234567890123456789-97-97-01234567890123456789
Fixes FINDING-034 and FINDING-035 from the reflex 0.11.0a1 pre-release testing (artifacts on branch claude/vibrant-pasteur-3921g3, prerelease-testing/runs/2026-11-11-v0.11.0a1/client_vars/ and verify_cv/).
All Submissions:
- Have you followed the guidelines stated in CONTRIBUTING.md?
- Have you checked to ensure there aren't any other open Pull Requests for the desired changed?
Type of change
- Bug fix (non-breaking change which fixes an issue)
- Breaking change (relative to 0.11.0a1 only:
$in names is rejected; one name with two defaults fails the compile) - This change requires a documentation update (included)
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?
Defects
rx.client_state is new public API in 0.11 (#6936); this makes its contract strict before 0.11.0 ships.
- FINDING-034: the "give it a name" hint and the
ClientStateVar.createdocstring showedrx.client_state("my_name"), which creates an unnamed var whose default is"my_name"(following the hint raised the same error). - FINDING-035: generated JS identifiers collided.
name="item"andname="Item"both declaredsetItem(Identifier setItem has already been declared: the memo module 500s in dev, the prod build fails); one name declared with two different defaults was silently resolved by whichever mounted first, or withauto_memoize=False500'd every route (Identifier openRxClientState has already been declared). Nothing was checked in Python;name="cs$0"was accepted and clashed with the first unnamed var.
Fix
- Hint and docstring now show
rx.client_state(default, name="my_name"). When an unnamed var's default looks like a name (the deprecated API's name-first order), the error raised bypush/retrieve/global_value/global_setsays so. Signature unchanged (namewas already keyword-only). - Collision-free bindings: getter
<name>RxClientState, setter<name>RxClientStateSet, updater placeholderprev<N>RxClientStateArg— injective in the name and mutually distinct (the old placeholder could also shadow a var namedprev0). - Name rule:
[A-Za-z_][A-Za-z0-9_]*(ASCII identifier without$, which is reserved for generated names);prefixuses the same validator; the error names the bad value. Every name in the docs passes. The deprecatedrx._x.client_state/ClientStateVar.createapply the same rule and keep their argument order and deprecation warnings. - Conflicting declarations: one name with two different defaults raises
ClientStateConflictError(a newReflexError/ValueErrorsubclass inreflex_base.utils.exceptions) at compile time, naming both declarations with file:line. The check runs as hooks are rendered (templates._render_hooks), across oneApp._compile(a fresh record per compile), so creating a var never raises and a dev hot reload with an edited default works. Identical re-declarations stay allowed (the documented "any component can read and write it" pattern). - Docs: one paragraph in
docs/wrapping-react/overview.mdon the name rule and the same-default rule.
Before / after:
item+Item:setItemdeclared twice →itemRxClientState, itemRxClientStateSetandItemRxClientState, ItemRxClientStateSet.rx.client_state("x", name="a-b")→name 'a-b' is not a valid client state name: it must be an ASCII identifier, a letter or underscore followed by letters, digits or underscores. It must not contain '$', which is reserved for generated names.- Two modules declaring
name="dup"with different defaults →Client state var 'dup' is declared with two different defaults: "dup-default" (declared at …/shared.py:15) and "OTHER-default" (declared at …/other_mod.py:6). …
Test plan
- Regression tests in
tests/units/reflex_base/test_client_state.py,tests/units/experimental/test_client_state.py,tests/units/test_app.py: 17 fail onmain(hint ×4 accessors, docstring, name-as-default hint, case-only identifiers, no duplicateconstin the compiled memo module, placeholder shadowing, name rejection ×4, non-string message, same-body conflict, legacy name rule, app-level two-page conflict naming both sites), e.g.assert 3 == 4 … {'ItemRxClientState','itemRxClientState','setItem'},DID NOT RAISE ValueError; allow-path tests (identical declarations, documented names, legacy positional name, conflict record not outliving a compile) pass before and after. Existing setter/placeholder spellings updated in ~20 assertions (test_client_state.py,test_memoize_plugin.py). - Full unit suite: 15663 passed, 66 skipped, 81.44% (the Playwright browser-driver module excluded locally for lack of a Chromium binary); touched modules rerun after the
ClientStateConflictErrorchange.ruff,pyright, pre-commit: clean. - End to end (dev and prod): the QA casing pages render (
/casing2used to 500), also withauto_memoize=False; hot reload with an edited default recompiles; the duplicate-default app fails at startup with the error above, the same-default variant renders all 5 routes; reflex-examplesoverkey(deprecatedClientStateVar.create,.set_value) 6/6 with its deprecation warning.
Notes
- Reported, not changed:
.set(value)/push(value)are not type-checked against the var's type;prefixis ignored whennameis given (documented); defaults that render differently (0vs0.0) count as a conflict; types are not compared.
News fragments: packages/reflex-base/news/+client-state-contract.bugfix.md, news/+client-state-contract.bugfix.md.
Generated by Claude Code