diff --git a/packages/reflex-base/news/6741.bugfix.md b/packages/reflex-base/news/6741.bugfix.md new file mode 100644 index 00000000000..44d9e398f9a --- /dev/null +++ b/packages/reflex-base/news/6741.bugfix.md @@ -0,0 +1 @@ +Fix `_get_all_hooks_internal` mutating each component's cached internal hooks with its descendants' hooks, which made memo tag hashes order-dependent and duplicated hooks into memo bodies. diff --git a/packages/reflex-base/src/reflex_base/components/component.py b/packages/reflex-base/src/reflex_base/components/component.py index 0bb0298feb6..f0f7917eb59 100644 --- a/packages/reflex-base/src/reflex_base/components/component.py +++ b/packages/reflex-base/src/reflex_base/components/component.py @@ -2136,8 +2136,9 @@ def _get_all_hooks_internal(self) -> dict[str, VarData | None]: Returns: The code that should appear just before user-defined hooks. """ - # Store the code in a set to avoid duplicates. - code = self._get_hooks_internal() + # Copy the cached dict from _get_hooks_internal so updating it with + # the children's hooks below does not pollute this node's cache. + code = dict(self._get_hooks_internal()) # Add the hook code for the children. for child in self.children: diff --git a/tests/units/components/test_component.py b/tests/units/components/test_component.py index 3aad6aaf53b..3325e11ac4f 100644 --- a/tests/units/components/test_component.py +++ b/tests/units/components/test_component.py @@ -2322,3 +2322,22 @@ def test_deepcopy_produces_independent_children() -> None: assert len(original.children) == 1 assert len(clone.children) == 2 + + +def test_get_all_hooks_internal_does_not_mutate_hooks_cache(): + """Collecting subtree hooks must not pollute each node's own hooks cache.""" + child = Box.create(id="hooks_cache_child") + parent = Box.create(child, id="hooks_cache_parent") + + parent_own_hooks = dict(parent._get_hooks_internal()) + child_own_hooks = dict(child._get_hooks_internal()) + combined = parent._get_all_hooks_internal() + + # The subtree collection includes both nodes' hooks. + for hook in (*parent_own_hooks, *child_own_hooks): + assert hook in combined + + # The parent's per-node cache must not absorb the child's hooks. + assert dict(parent._get_hooks_internal()) == parent_own_hooks + # And repeated collection yields the same result. + assert parent._get_all_hooks_internal() == combined