diff --git a/reflex/experimental/memo.py b/reflex/experimental/memo.py index 3c88aca6cff..776e056cc50 100644 --- a/reflex/experimental/memo.py +++ b/reflex/experimental/memo.py @@ -1048,6 +1048,14 @@ def passthrough(children: Var[Component]) -> Component: new_component = copy(component) if render_snapshot: return new_component + # Components with no original structural children own their own JSX + # output (e.g. ``CodeBlock`` injects ``code`` as the ``children`` prop + # in ``_render``). Substituting a ``{children}`` hole here would emit + # ``jsx(Inner, {children: "..."}, hole)``, and an undefined hole at + # call time clobbers the prop. Skip the substitution so the wrapper's + # ``children`` parameter is present in the signature but unused. + if not component.children: + return new_component hole_bare = Bare.create(children) captured_hole_child.append(hole_bare) # Substitute the ``{children}`` hole for the original descendants so diff --git a/tests/units/compiler/test_memoize_plugin.py b/tests/units/compiler/test_memoize_plugin.py index a715b930436..a23c5c9cc67 100644 --- a/tests/units/compiler/test_memoize_plugin.py +++ b/tests/units/compiler/test_memoize_plugin.py @@ -75,6 +75,18 @@ class LeafComponent(Component): _memoization_mode = MemoizationMode(recursive=False) +class ChildrenViaProp(Component): + """Stub mirroring ``CodeBlock`` — injects its content as ``children`` prop.""" + + tag = "ChildrenViaProp" + library = "children-via-prop-lib" + + code: Var[str] = component_field(default=LiteralVar.create("")) + + def _render(self): + return super()._render().remove_props("code").add_props(children=self.code) + + class SpecialFormMemoState(BaseState): items: Field[list[str]] = field(default_factory=lambda: ["a"]) flag: Field[bool] = field(default=True) @@ -417,6 +429,23 @@ def test_generated_memo_component_is_not_itself_memoized() -> None: assert not _should_memoize(wrapper) +def test_passthrough_memo_skips_hole_for_childless_component() -> None: + """Childless components own their JSX output, so the wrapper must not + inject a ``{children}`` hole. + + Regression: components like ``CodeBlock`` set ``children`` on their own + rendered Tag via ``_render``. Substituting a ``Bare({children})`` hole + would emit ``jsx(Inner, {children: "..."}, hole)``, and at call time the + undefined hole arg overwrites ``props.children`` under Emotion's jsx + semantics — causing every reactive ``rx.code_block`` to render an empty + ```` element. + """ + component = ChildrenViaProp.create(code=STATE_VAR) + assert not component.children + _wrapper_factory, definition = create_passthrough_component_memo(component) + assert definition.passthrough_hole_child is None + + def test_event_trigger_memoization_not_emit_usecallback_in_page_hooks() -> None: """Components with event triggers do not get useCallback wrappers at the page level.""" from reflex_base.event import EventChain