fix(compiler): check memo modules for state reads when state is disabled
All Submissions:
- Have you followed the guidelines stated in CONTRIBUTING.md file?
- 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)
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?
What
With rx.App(enable_state=False), compile_app is 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_components and scans every module in compile_results, which at that point holds the page modules and all memo modules. code_uses_state_contexts itself 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:
- Delete the guard (option 1 in the issue). As noted in the issue, the unsupported state use would then only fail later, at runtime.
- Collect
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. - Structural validation from the originating Vars across pages, memos, app wraps and the hydrate fallback. This is the more robust fix, but it is a much larger change. It can still be done later; this PR doesn't get in its way.
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: MEMOS is compiled as a whole, so an @rx.memo that reads state is now rejected in an enable_state=False app 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 real compile_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.- The same five compile when state is enabled.
- A stateless app with
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 in compile_app.
Also ran test_var.py::test_code_uses_state_contexts, test_compiler.py, test_memoize_plugin.py and test_app.py (434 passed), and ruff check, ruff format --check, pyright and pre-commit on the changed files.
closes #7579