Skip to content

Auto-memo passthrough modules pull in descendants' imports unnecessarily #6429

@masenf

Description

@masenf

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

  1. 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.
  2. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions