Repository navigation
Conversation
The enable_state=False guard only scanned page modules, but auto-memoization moves state reads into memo modules that are compiled afterwards. Run the guard once the memo modules exist so reads in memoized components, rx.memo functions, app wraps and the hydrate fallback are rejected too. Fixes reflex-dev#7579
[Medium impact] The change affects frontend compilation for apps that disable state. The app-root gap should be fixed before merging so state-reading wraps cannot bypass the intended compile error. Findings
SummaryMoves the disabled-state check after memo compilation so it also catches state reads moved out of page modules.
Acknowledged limits excluded from findings:
Reviews (1) · Last reviewed commit: "fix(compiler): check memo modules for st..." · Reviewed by Greptile |
| if app._state is None and any( | ||
| code_uses_state_contexts(code) for _, code in compile_results |
There was a problem hiding this comment.
State-reading wraps escape the check
An app wrap that opts out of memoization still escapes this check. For example, put UnmemoizedLabel.create(label=DynState.label) in app.extra_app_wraps with enable_state=False and a static page. MemoizationDisposition.NEVER keeps its state hook in app_root, but compile_app_root runs after this check. Compilation therefore succeeds and leaves the missing-state failure for the browser. Check the compiled app root too, and add this case to the tests.
Knowledge Base Used: Frontend compilation pipeline
All Submissions:
Type of change
Changes To Core Features:
What
With
rx.App(enable_state=False),compile_appis supposed to raise "To access rx.State in frontend components, at least one subclass of rx.State must be defined in the app." when a component reads state. The check ran right after the pages were compiled and only looked at page output. Auto-memoization moves most state reads into memo modules, and those are compiled later, together with app-wrap memos and the hydrate fallback memo. So the issue's example (rx.text(DynState.label)) compiled without error and failed at runtime.The check now runs after
compile_memo_componentsand scans every module incompile_results, which at that point holds the page modules and all memo modules.code_uses_state_contextsitself is unchanged.Why this direction
I went for the smallest change that keeps the existing guard logic: same predicate, same error, just run over all the compiled JS that can subscribe to state instead of the pages only.
Alternatives I considered:
VarData.statefrom the compiled hooks (option 2). As @masenf pointed out, this misses the reported example, because_get_vars_hookskeeps only position metadata in the hook entries.Known limits, unchanged by this PR: the check still matches the
useStateContext(substring, so literal text containing it is still a false positive (now in memo modules too). One behavior change:MEMOSis compiled as a whole, so an@rx.memothat reads state is now rejected in anenable_state=Falseapp even if no page renders it. Before, it was emitted but never imported.Tests
New cases in
tests/units/compiler/test_compiler.py, using the realcompile_app(app, dry_run=True, use_rich=False)path:enable_state=Falsewith a state read raises for: an auto-memoized page component, a non-memoized page component, anrx.memobody, an app wrap, and the hydrate fallback.enable_state=Falsecompiles, in dev and in prod.Without the fix, 4 cases fail with
DID NOT RAISE ReflexRuntimeError(auto memo, explicit memo, app wrap, hydrate fallback); the non-memoized page case already raised. With the fix, all 12 pass. The new tests cover every changed line incompile_app.Also ran
test_var.py::test_code_uses_state_contexts,test_compiler.py,test_memoize_plugin.pyandtest_app.py(434 passed), andruff check,ruff format --check,pyrightandpre-commiton the changed files.closes #7579