Skip to content

fix(compiler): check memo modules for state reads when state is disabled - #7630

Open
fixfon wants to merge 1 commit into
reflex-dev:mainfrom
fixfon:fix/7579-memo-state-guard
Open

fixfon wants to merge 1 commit into
reflex-dev:mainfrom
fixfon:fix/7579-memo-state-guard

Conversation

@fixfon

@fixfon fixfon commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

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.state from the compiled hooks (option 2). As @masenf pointed out, this misses the reported example, because _get_vars_hooks keeps 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=False with a state read raises for: an auto-memoized page component, a non-memoized page component, an rx.memo body, an app wrap, and the hydrate fallback.
  • The same five compile when state is enabled.
  • A stateless app with enable_state=False compiles, 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

View guided diff

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
@fixfon
fixfon requested a review from a team as a code owner October 22, 2026 00:11

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 3 files

View guided diff | Re-trigger cubic

@greptile-apps

greptile-apps Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[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

  1. P1 State-reading wraps escape the check ▶

Summary

Moves the disabled-state check after memo compilation so it also catches state reads moved out of page modules.

  • The compiler catches state reads moved into memoized components.

Acknowledged limits excluded from findings:

  • fixfon acknowledges substring false positives, including literal text in memo modules; the existing predicate is intentionally unchanged.
  • fixfon acknowledges that unused registered state-reading memos are now rejected because MEMOS is compiled as a whole.

Reviews (1) · Last reviewed commit: "fix(compiler): check memo modules for st..." · Reviewed by Greptile

Comment on lines +1404 to +1405
if app._state is None and any(
code_uses_state_contexts(code) for _, code in compile_results

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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

@masenf masenf added the bug Something isn't working label Oct 22, 2026 — with ChatGPT Codex Connector

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

enable_state=False guard (code_uses_state_contexts) misses memoized state reads

2 participants