Skip to content

Auto-memoize user @rx.memo components with stateful props - #6949

Merged
masenf merged 3 commits into
mainfrom
claude/auto-memoize-stateful-props-dqga0l
Aug 27, 2026
Merged

Auto-memoize user @rx.memo components with stateful props#6949
masenf merged 3 commits into
mainfrom
claude/auto-memoize-stateful-props-dqga0l

Conversation

@masenf

@masenf masenf commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Type of change

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

Description

Extends the compiler's auto-memoization pass to wrap user-authored @rx.memo components when they're called with state-bound props or event handlers. This ensures state hooks compile into a generated wrapper instead of the page module, preventing unnecessary full-page re-renders on state changes.

Key changes:

  1. Auto-memo wrapper generation (reflex/compiler/plugins/memoize.py):

    • Modified _should_memoize() to treat @rx.memo components as eligible for wrapping when bound to state
    • Generated wrappers opt out of being wrapped again via MemoizationDisposition.NEVER
  2. Memo component definition (packages/reflex-base/src/reflex_base/components/memo.py):

    • Added auto_memo_wrapper flag to MemoComponentDefinition to distinguish compiler-generated wrappers from user code
    • Updated _get_memo_component_class() to apply NEVER disposition only to auto-generated wrappers, allowing user @rx.memo components to be wrapped when needed
    • Clarified docstring explaining the interaction between @rx.memo and auto-memoization
  3. Passthrough wrapper creation (packages/reflex-base/src/reflex_base/components/memo.py):

    • Set auto_memo_wrapper=True when creating passthrough wrappers via create_passthrough_component_memo()
  4. Documentation (docs/library/other/memo.md):

    • Added explanation of how state binding at call sites works with auto-memoization

Behavior

Before: A @rx.memo component called with a state Var would pull that state hook into the page module, causing the entire page to re-render on state changes.

After: The compiler detects this pattern and generates a wrapper that holds the state hooks. The page renders the wrapper, which re-renders on state changes and passes the prop to the memoized component. React's memo then skips the component unless the prop value actually changed.

Testing

  • Added 6 comprehensive unit tests covering:
    • Auto-memo wrapper opt-out behavior
    • User memo with stateful props gets wrapped
    • User memo with static props stays inline
    • Event handler props use useCallback in wrapper
    • Children render in page scope, not captured
    • Memo inside foreach renders in foreach body
  • Added 1 integration test (test_memo_stateful_prop_and_children_update) verifying the feature works end-to-end with both stateful props and children

All tests pass with adequate coverage.

Checklist

  • Tests pass with adequate coverage
  • uv run ruff check . and uv run ruff format . clean
  • uv run pyright reflex tests passes
  • pyi_hashes.json updated
  • Documentation updated

https://claude.ai/code/session_01FNfPvVUGGMUQHh63V4Phbe

Review in cubic

`MemoComponent` opted out of auto-memoization wholesale via
`MemoizationDisposition.NEVER`, so binding a state Var (or an event
handler) to a prop at the call site left the state `useContext` in the
page module. React's `memo` then only spared the memoized component's own
subtree: every state change still re-rendered the page and every static
sibling in it.

Move the opt-out from `MemoComponent` to just the wrappers the
auto-memoize pass generates (they already are the boundary, and wrapping
them would recurse), tracked by a new `auto_memo_wrapper` flag on
`MemoComponentDefinition` that `_get_memo_component_class` turns into the
`NEVER` disposition. User-authored memos now run through the normal
heuristic: static props stay inline, while state-bound props and event
handlers get a generated wrapper that holds the hooks.

The page then carries no dependency on the state, the wrapper absorbs the
re-render, and React's `memo` stops there unless a prop value actually
changed — so a call site can punch a single Var dependency through to an
expensive component.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FNfPvVUGGMUQHh63V4Phbe
@masenf
masenf requested review from a team and Alek99 as code owners August 26, 2026 05:25
@greptile-apps

greptile-apps Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR extends compiler auto-memoization to isolate stateful props and event handlers passed to user-authored @rx.memo components.

  • Marks compiler-generated passthrough wrappers so they cannot be wrapped recursively.
  • Keeps static memo calls inline while extracting stateful calls into generated wrappers.
  • Adds unit, integration, documentation, and release-note coverage for the new behavior.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
packages/reflex-base/src/reflex_base/components/memo.py Adds explicit generated-wrapper metadata and conditionally applies the recursion-prevention memoization disposition.
reflex/compiler/plugins/memoize.py Allows user-authored memo components to pass through the existing stateful auto-memoization heuristic.
tests/units/compiler/test_memoize_plugin.py Covers wrapper opt-out, stateful and static props, event callbacks, child passthrough, and foreach ownership.
tests/integration/tests_playwright/test_memo.py Verifies that stateful memo props and children update correctly in the browser.
docs/library/other/memo.md Documents how generated wrappers isolate call-site state dependencies.

Reviews (3): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

@codspeed-hq

codspeed-hq Bot commented Aug 26, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 27 untouched benchmarks
⏩ 8 skipped benchmarks1


Comparing claude/auto-memoize-stateful-props-dqga0l (d982b97) with main (a4d99be)

Open in CodSpeed

Footnotes

  1. 8 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@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.

All reported issues were addressed across 6 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread docs/library/other/memo.md
Add news fragments for the reflex and reflex-base packages, and correct
the docs paragraph: the page function no longer re-runs, but a sibling
reading the same state still re-renders inside its own generated wrapper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FNfPvVUGGMUQHh63V4Phbe
FarhanAliRaza
FarhanAliRaza previously approved these changes Aug 27, 2026
…tateful-props-dqga0l

# Conflicts:
#	packages/reflex-base/src/reflex_base/components/memo.py
#	pyi_hashes.json
@masenf
masenf merged commit 603d303 into main Aug 27, 2026
112 checks passed
@masenf
masenf deleted the claude/auto-memoize-stateful-props-dqga0l branch August 27, 2026 20:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants