Context
Follow-up from the compiler-hooks branch work. Not blocking that PR — capturing here so we can address it as a separate change.
Observation
When a component A is auto-memoized as a passthrough wrapper ({children} hole) and contains a stateful child B that is also auto-memoized, the generated module for A ends up importing B's memo wrapper, even though A's body is just {children} and B only appears at the page level (as the actual value passed in for A's children).
Cause
In reflex/compiler/utils.py, compile_experimental_component_memo — passthrough branch:
hole_child = definition.passthrough_hole_child
if hole_child is not None:
render = copy.copy(definition.component)
_apply_root_style(render)
hooks = _root_only_hooks(render)
custom_code = _root_only_custom_code(render)
dynamic_imports = _root_only_dynamic_imports(render)
# ...
all_imports = render._get_all_imports() # walks the whole subtree
render.children = [hole_child] # hole swap happens AFTER
rendered = render.render()
Hooks / custom code / dynamic imports are correctly limited to the root via the _root_only_* helpers (descendants render at the page level via {children}), but _get_all_imports() walks the full descendant subtree. For A wraps B where both are auto-memoized, B's wrapper sits inside A's definition.component.children, so its $/utils/components/<B_memo> import gets pulled into A's module even though A's rendered body never references B.
The block comment in that function deliberately over-includes imports as a safety net for add_hooks strings that reference symbols (refs, StateContexts, ...) whose imports normally reach the module via descendants. That's a real concern — but for child-hole passthroughs specifically, the descendants are exactly the user-passed children that render at the page level, so most of those imports really are dead in this module.
Possible directions
- Narrow the descendant scan to the root's own machinery. Collect from
component._get_imports() plus its own props / Vars / event-trigger var_data, and skip the children walk. Risk: a root add_hooks string that references a symbol only imported by a descendant breaks. Could mitigate by also walking the root's _get_hooks_imports() rather than the full subtree.
- Filter auto-memo wrapper imports out of the subtree-collected set. Keep
_get_all_imports() but drop entries whose lib is $/utils/components/<name> for any name in compile_context.auto_memo_components. Smaller blast radius, hits exactly this case, but doesn't help the broader "descendants drag in unrelated imports" issue.
Acceptance
- Regression test: page renders an auto-memoized passthrough
A whose page-level child is an auto-memoized B. Assert that A's generated memo module's imports do not reference B's wrapper module.
- No regression in
add_hooks-based components that legitimately depend on descendant-supplied imports (e.g. things that emit hook strings referencing refs / state contexts).
Context
Follow-up from the
compiler-hooksbranch work. Not blocking that PR — capturing here so we can address it as a separate change.Observation
When a component
Ais auto-memoized as a passthrough wrapper ({children}hole) and contains a stateful childBthat is also auto-memoized, the generated module forAends up importingB's memo wrapper, even thoughA's body is just{children}andBonly appears at the page level (as the actual value passed in forA's children).Cause
In
reflex/compiler/utils.py,compile_experimental_component_memo— passthrough branch:Hooks / custom code / dynamic imports are correctly limited to the root via the
_root_only_*helpers (descendants render at the page level via{children}), but_get_all_imports()walks the full descendant subtree. ForAwrapsBwhere both are auto-memoized,B's wrapper sits insideA'sdefinition.component.children, so its$/utils/components/<B_memo>import gets pulled intoA's module even thoughA's rendered body never referencesB.The block comment in that function deliberately over-includes imports as a safety net for
add_hooksstrings that reference symbols (refs,StateContexts, ...) whose imports normally reach the module via descendants. That's a real concern — but for child-hole passthroughs specifically, the descendants are exactly the user-passed children that render at the page level, so most of those imports really are dead in this module.Possible directions
component._get_imports()plus its own props / Vars / event-trigger var_data, and skip the children walk. Risk: a rootadd_hooksstring that references a symbol only imported by a descendant breaks. Could mitigate by also walking the root's_get_hooks_imports()rather than the full subtree._get_all_imports()but drop entries whose lib is$/utils/components/<name>for any name incompile_context.auto_memo_components. Smaller blast radius, hits exactly this case, but doesn't help the broader "descendants drag in unrelated imports" issue.Acceptance
Awhose page-level child is an auto-memoizedB. Assert thatA's generated memo module's imports do not referenceB's wrapper module.add_hooks-based components that legitimately depend on descendant-supplied imports (e.g. things that emit hook strings referencingrefs/ state contexts).