Collect app wraps from the whole @rx.memo body
closes #7174
A memo's body compiles into its own JS module, so the call site is represented in the page tree only by a generated wrapper component and the body subtree is invisible to every app-wrap collector. The wrapper forwarded exactly one thing: the static _get_app_wrap_components of the body's root class, copied onto the wrapper class by _get_memo_component_class.
Everything else was dropped:
- A provider requested by a nested descendant never reached the app root, so
rx.box(NeedsProvider())lost whatNeedsProvider()kept. - Providers requested through
VarData.app_wrapswere dropped at any depth, including the root โ which is howrx.uploadasks forUploadFilesProvider.useContext(UploadFilesContext)then resolved to its default value and file selection silently did nothing. - Components whose
create()returns a wrapper element lost theirs too:rx.data_editorreturns aDiv, so its portal app wrap went missing even as the memo's root.
Not specific to any one component โ it affects anything requesting an app wrap, including third-party and reflex-enterprise components (the drag-and-drop provider is the case that surfaced it).
Changes
Each wrapper class now gets a _get_app_wrap_components that reports what the body it stands in for requires, class-declared and var-declared alike. A distinct function per class is load-bearing: the page collector dedupes its scan by type(comp)._get_app_wrap_components identity, so a shared function would let only the first memo on a page contribute. The result is computed once per class, and reset_memo_component_classes() rebuilds classes each compile, so it can't go stale.
Passthrough wrappers still report only their root โ their descendants render at the call site, where the page walk already collects them.
Routing this through _get_app_wrap_components fixes both collection surfaces at once: the compiler plugin walk for pages, and Component._get_all_app_wrap_components, which App._app_root uses to expand the app-wrap chain (a chain that itself contains memo components โ the toaster provider, the sticky badge).
The var-app-wrap collectors move from reflex.compiler.plugins.builtin to a new reflex_base.components.app_wraps so memo.py can share them instead of duplicating the walk. They only ever touched Component and VarData, never the compiler, so this is a relocation with no behavior change; builtin.py, memoize.py and compiler.py import from the new home.
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?
Regression tests in tests/units/components/test_memo.py cover nested and root class-declared wraps, var-declared wraps (rx.upload inside a memo), the per-class function identity the collector's dedupe depends on, and the _get_all_app_wrap_components path. tests/units/test_app.py adds the end-to-end page-compile assertion.
Verified locally: uv run pytest tests/units passes (77.53% coverage; the only failures are the pre-existing tests/units/reflex_cli ones, unchanged on a clean tree), uv run ruff check ., uv run ruff format . and uv run pyright reflex tests are clean, and scripts/make_pyi.py produces no changes.
๐ค Generated with Claude Code
https://claude.ai/code/session_01FFRsBUWSsWQCPKPTZ8yxT1
Generated by Claude Code