From 04155c45c8ff655bb3ee25e3d0a1d12ce82af781 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 20:02:21 +0000 Subject: [PATCH 1/2] Fix _get_all_hooks_internal mutating the shared per-node hooks cache _get_all_hooks_internal called .update() directly on the dict returned by _get_hooks_internal, which is the node's cached _hooks_internal_cache. Walking a tree therefore polluted every node's cache with its descendants' hooks, making hook collection order-dependent: memo tag hashes over internal hooks could differ for identical subtrees (defeating dedup) and per-node hook queries returned descendant hooks. Copy the cached dict before merging children's hooks into it. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01G8cXh3TUbNtbjm2ERqE62X --- .../src/reflex_base/components/component.py | 5 +++-- tests/units/components/test_component.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) 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 From c537b6167ec9a15b72c78eebb0073dbbbeb0adb1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 20:06:34 +0000 Subject: [PATCH 2/2] Add changelog fragment Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01G8cXh3TUbNtbjm2ERqE62X --- packages/reflex-base/news/6741.bugfix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/reflex-base/news/6741.bugfix.md 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.