From be22a5c5b946c25f85f0b570787cb15e4f19f23e Mon Sep 17 00:00:00 2001 From: LeonxLJX <51880185+LeonxLJX@users.noreply.github.com> Date: Sat, 29 Aug 2026 11:54:06 +0800 Subject: [PATCH 1/3] fix: avoid duplicating inner tag in MemoComponent wrapper tag Override _compute_memo_tag on MemoComponent to skip the self.tag segment, which is already embedded in the dynamic subclass __qualname__ (MemoComponent_). Previously the wrapper tag contained the inner tag twice (e.g. Memocomponent_card_98ffd1e1_card_98ffd1e1_). Adds a regression test asserting the inner tag appears exactly once. Closes #6955 --- .../src/reflex_base/components/memo.py | 28 ++++++++++++++++ tests/units/compiler/test_memoize_plugin.py | 32 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/packages/reflex-base/src/reflex_base/components/memo.py b/packages/reflex-base/src/reflex_base/components/memo.py index 133afe39876..638a7e34d55 100644 --- a/packages/reflex-base/src/reflex_base/components/memo.py +++ b/packages/reflex-base/src/reflex_base/components/memo.py @@ -398,6 +398,34 @@ def _validate_component_children(self, children: list[Component]) -> None: children: The children of the component (ignored). """ + def _compute_memo_tag(self) -> str: + """Compute a stable tag name for this memo component. + + Overrides ``Component._compute_memo_tag`` to avoid duplicating the + wrapped component's tag. For a ``MemoComponent`` the dynamic subclass + ``__qualname__`` is ``MemoComponent_`` (see + :func:`_get_memo_component_class`), which already encodes the inner + tag. Appending ``self.tag`` again would produce the inner tag twice + (e.g. ``Memocomponent_card_98ffd1e1_card_98ffd1e1_``). + + The class identity is still preserved via the qualname prefix, so the + collision guarantee from the base implementation holds. + + Returns: + The stable tag name. + """ + from reflex_base.components.memoize_helpers import ( + MemoizationStrategy, + get_memoization_strategy, + ) + + comp_hash = self._get_component_hash( + shallow=get_memoization_strategy(self) == MemoizationStrategy.PASSTHROUGH + ) + return format.format_state_name( + f"{type(self).__qualname__}_{comp_hash}" + ).capitalize() + def _post_init(self, **kwargs): """Initialize the memo component. diff --git a/tests/units/compiler/test_memoize_plugin.py b/tests/units/compiler/test_memoize_plugin.py index 0098e49bd76..7b8b0e4dcbf 100644 --- a/tests/units/compiler/test_memoize_plugin.py +++ b/tests/units/compiler/test_memoize_plugin.py @@ -2593,3 +2593,35 @@ def test_each_memo_wrapper_emits_one_component_module_file() -> None: "for Plain, one for WithProp, and one snapshot wrapper for the " f"LeafComponent boundary. Got: {sorted(ctx.memoize_wrappers)}" ) + + +def test_memo_wrapper_tag_contains_inner_tag_once() -> None: + """Auto-memo wrapper tags must not duplicate the wrapped component's tag. + + Regression test for #6955: ``MemoComponent._compute_memo_tag`` previously + concatenated ``type(self).__qualname__`` (which already embeds the inner + tag as ``MemoComponent_``) with ``self.tag`` again, producing + names like ``Memocomponent_plain_abc123_plain_abc123_``. The inner + tag must appear exactly once in the wrapper tag. + """ + ctx, _page_ctx = _compile_single_page(lambda: Plain.create(STATE_VAR)) + + assert len(ctx.memoize_wrappers) == 1, ( + f"Expected exactly one auto-memo wrapper, got: {sorted(ctx.memoize_wrappers)}" + ) + wrapper_tag = next(iter(ctx.memoize_wrappers)) + # The inner component's tag is "Plain"; after format_state_name it becomes + # lowercase "plain" in the wrapper tag. Count occurrences of the inner tag + # segment (case-insensitive, word-bounded to avoid matching hash substrings). + inner_tag_lower = Plain.tag.lower() + # Split on underscores and count exact matches of the inner tag + segments = wrapper_tag.lower().split("_") + inner_tag_occurrences = segments.count(inner_tag_lower) + assert inner_tag_occurrences == 1, ( + f"Inner tag '{Plain.tag}' should appear exactly once in wrapper tag, " + f"found {inner_tag_occurrences} times. wrapper_tag={wrapper_tag}" + ) + # The wrapper tag should still start with the MemoComponent prefix + assert wrapper_tag.lower().startswith("memocomponent"), ( + f"Wrapper tag should keep the MemoComponent prefix, got: {wrapper_tag}" + ) From 11d8c74682999eb6c8063584137f3fd79a1b1735 Mon Sep 17 00:00:00 2001 From: LeonxLJX <51880185+LeonxLJX@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:44:00 +0800 Subject: [PATCH 2/3] chore: add reflex-base news fragment for memo tag fix Adds the required package news fragment so changelog validation passes for the user-facing memo-wrapper tag fix. --- packages/reflex-base/news/6955.bugfix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/reflex-base/news/6955.bugfix.md diff --git a/packages/reflex-base/news/6955.bugfix.md b/packages/reflex-base/news/6955.bugfix.md new file mode 100644 index 00000000000..0d6f44a7e97 --- /dev/null +++ b/packages/reflex-base/news/6955.bugfix.md @@ -0,0 +1 @@ +Auto-memoized `@rx.memo` wrapper tags no longer duplicate the wrapped component's tag. `MemoComponent._compute_memo_tag` now skips the `self.tag` segment, which is already embedded in the dynamic subclass `__qualname__` (`MemoComponent_`), so generated wrapper names read as `Memocomponent__` instead of `Memocomponent___`. From 77837d31f542ab6884f0189dbf10e225ce2ab0a7 Mon Sep 17 00:00:00 2001 From: LeonxLJX <51880185+LeonxLJX@users.noreply.github.com> Date: Tue, 1 Sep 2026 17:53:56 +0800 Subject: [PATCH 3/3] chore: add reflex-base news fragment for #7004 --- news/7004.bugfix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/7004.bugfix.md diff --git a/news/7004.bugfix.md b/news/7004.bugfix.md new file mode 100644 index 00000000000..337db34b169 --- /dev/null +++ b/news/7004.bugfix.md @@ -0,0 +1 @@ +Memo wrappers no longer duplicate the wrapped component's tag when computing the generated component name, so `MemoComponent` instances get stable, collision-free names instead of a doubled tag prefix.