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
masenf pushed a commit that referenced this pull request Aug 28, 2026
Sample apps, Playwright drivers, NOTES.md and evidence screenshots from
end-to-end browser testing of the routing fixes (#6593 on_load
supersedes, #6790 splat matching, #6953 static/dynamic siblings, #6919
chained-event routing) and the rx.memo changes (#6949 call-site
auto-memoization, #6605 RestProp style classification, #6945
displayName, #6730 wrapper=). Both clusters pass in dev and prod against
the published PyPI alphas, with 0.9.8 baseline comparisons reproducing
the old bugs. Anomalies recorded in each NOTES.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
masenf pushed a commit that referenced this pull request Aug 28, 2026
A ~29s 1920x1080 motion graphic covering the end-user-facing changes in
0.9.9, sourced from the 0.9.9a1 and 0.9.9a2 changelog entries across
reflex, reflex-base, the component packages and reflex-hosting-cli.

Nine beats: the dev-mode main-thread win (#6905), call-site
auto-memoization of @rx.memo (#6949), the move to React Router 8 (#6854),
DevTools component naming (#6945), stdlib logging with --json plus the
client_error report (#6863, #6865, #6827), the cloud CLI's whoami/token
and autoscaling flags (#6918, #6884, #6948), and a grid of the smaller
fixes a user would notice (#6786, #6593, #6944, #6971, #6790, #6920).

Styled to match reflex.dev by reusing its design system rather than
approximating it: the dark violet/slate scales, wave-line motif, radial
bloom and type scale come from reflex-site-shared, and the fonts are the
same @fontsource-variable Instrument Sans and JetBrains Mono files the
site serves.

Source only — the rendered mp4 is a build artifact, regenerated by
`npm run build` and ignored. Lives outside the packaged tree (hatch only
includes /reflex), so it does not affect the published wheel or sdist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016arkVKmoogzUtZNfZZSvSo
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