Skip to content

Commit 373d98a

Browse files
authored
fix(memo): skip {children} hole substitution when wrapped component h… (#6466)
* fix(memo): skip {children} hole substitution when wrapped component has no structural children * add test
1 parent 1ffc3b5 commit 373d98a

2 files changed

Lines changed: 37 additions & 0 deletions

File tree

reflex/experimental/memo.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,6 +1048,14 @@ def passthrough(children: Var[Component]) -> Component:
10481048
new_component = copy(component)
10491049
if render_snapshot:
10501050
return new_component
1051+
# Components with no original structural children own their own JSX
1052+
# output (e.g. ``CodeBlock`` injects ``code`` as the ``children`` prop
1053+
# in ``_render``). Substituting a ``{children}`` hole here would emit
1054+
# ``jsx(Inner, {children: "..."}, hole)``, and an undefined hole at
1055+
# call time clobbers the prop. Skip the substitution so the wrapper's
1056+
# ``children`` parameter is present in the signature but unused.
1057+
if not component.children:
1058+
return new_component
10511059
hole_bare = Bare.create(children)
10521060
captured_hole_child.append(hole_bare)
10531061
# Substitute the ``{children}`` hole for the original descendants so

tests/units/compiler/test_memoize_plugin.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,18 @@ class LeafComponent(Component):
7575
_memoization_mode = MemoizationMode(recursive=False)
7676

7777

78+
class ChildrenViaProp(Component):
79+
"""Stub mirroring ``CodeBlock`` — injects its content as ``children`` prop."""
80+
81+
tag = "ChildrenViaProp"
82+
library = "children-via-prop-lib"
83+
84+
code: Var[str] = component_field(default=LiteralVar.create(""))
85+
86+
def _render(self):
87+
return super()._render().remove_props("code").add_props(children=self.code)
88+
89+
7890
class SpecialFormMemoState(BaseState):
7991
items: Field[list[str]] = field(default_factory=lambda: ["a"])
8092
flag: Field[bool] = field(default=True)
@@ -417,6 +429,23 @@ def test_generated_memo_component_is_not_itself_memoized() -> None:
417429
assert not _should_memoize(wrapper)
418430

419431

432+
def test_passthrough_memo_skips_hole_for_childless_component() -> None:
433+
"""Childless components own their JSX output, so the wrapper must not
434+
inject a ``{children}`` hole.
435+
436+
Regression: components like ``CodeBlock`` set ``children`` on their own
437+
rendered Tag via ``_render``. Substituting a ``Bare({children})`` hole
438+
would emit ``jsx(Inner, {children: "..."}, hole)``, and at call time the
439+
undefined hole arg overwrites ``props.children`` under Emotion's jsx
440+
semantics — causing every reactive ``rx.code_block`` to render an empty
441+
``<code>`` element.
442+
"""
443+
component = ChildrenViaProp.create(code=STATE_VAR)
444+
assert not component.children
445+
_wrapper_factory, definition = create_passthrough_component_memo(component)
446+
assert definition.passthrough_hole_child is None
447+
448+
420449
def test_event_trigger_memoization_not_emit_usecallback_in_page_hooks() -> None:
421450
"""Components with event triggers do not get useCallback wrappers at the page level."""
422451
from reflex_base.event import EventChain

0 commit comments

Comments
 (0)