From c95ce224bad9dae54e527d6eed33dbd392563677 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Thu, 16 Jul 2026 19:54:19 +0200 Subject: [PATCH] fix: make Moment a MemoizationLeaf --- .../news/+moment-memoization-leaf.bugfix.md | 1 + .../src/reflex_components_moment/moment.py | 4 +- pyi_hashes.json | 2 +- tests/units/compiler/test_memoize_plugin.py | 55 +++++++++++++++++++ 4 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 packages/reflex-components-moment/news/+moment-memoization-leaf.bugfix.md diff --git a/packages/reflex-components-moment/news/+moment-memoization-leaf.bugfix.md b/packages/reflex-components-moment/news/+moment-memoization-leaf.bugfix.md new file mode 100644 index 00000000000..23a18964f92 --- /dev/null +++ b/packages/reflex-components-moment/news/+moment-memoization-leaf.bugfix.md @@ -0,0 +1 @@ +Make `rx.moment` a `MemoizationLeaf` so a stateful date child is not memo-wrapped, which react-moment parsed like `moment({})` (today at midnight). diff --git a/packages/reflex-components-moment/src/reflex_components_moment/moment.py b/packages/reflex-components-moment/src/reflex_components_moment/moment.py index 84988075451..067c9d9ea63 100644 --- a/packages/reflex-components-moment/src/reflex_components_moment/moment.py +++ b/packages/reflex-components-moment/src/reflex_components_moment/moment.py @@ -5,7 +5,7 @@ import dataclasses import datetime -from reflex_base.components.component import NoSSRComponent, field +from reflex_base.components.component import MemoizationLeaf, NoSSRComponent, field from reflex_base.event import EventHandler, passthrough_event_spec from reflex_base.utils.imports import ImportDict from reflex_base.vars.base import LiteralVar, Var @@ -26,7 +26,7 @@ class MomentDelta: milliseconds: int | None = dataclasses.field(default=None) -class Moment(NoSSRComponent): +class Moment(NoSSRComponent, MemoizationLeaf): """The Moment component.""" tag: str | None = "Moment" diff --git a/pyi_hashes.json b/pyi_hashes.json index 1ec6e0eb1d6..481c3e8ef8b 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -41,7 +41,7 @@ "packages/reflex-components-gridjs/src/reflex_components_gridjs/datatable.pyi": "2ce1c076ecf5c2fa4945b4abdbf2f91d", "packages/reflex-components-lucide/src/reflex_components_lucide/icon.pyi": "2e1da186a37e2bb8a1d90e16ee9a63b5", "packages/reflex-components-markdown/src/reflex_components_markdown/markdown.pyi": "79d0a59b1ba12a2f2c4a09fa6b5c776f", - "packages/reflex-components-moment/src/reflex_components_moment/moment.pyi": "eabf233471bc5b94084914f6f35ecd66", + "packages/reflex-components-moment/src/reflex_components_moment/moment.pyi": "85d515f5254bb4c188075873ec4d8a51", "packages/reflex-components-plotly/src/reflex_components_plotly/plotly.pyi": "80b36863336e53c050cc61386f1c9271", "packages/reflex-components-radix/src/reflex_components_radix/__init__.pyi": "a77352f60fb6f4135b5d08a6e56efa6d", "packages/reflex-components-radix/src/reflex_components_radix/primitives/__init__.pyi": "bbd4d1a4fa73275a882c33ba485d0165", diff --git a/tests/units/compiler/test_memoize_plugin.py b/tests/units/compiler/test_memoize_plugin.py index b9fb1c5df51..4f47acc1120 100644 --- a/tests/units/compiler/test_memoize_plugin.py +++ b/tests/units/compiler/test_memoize_plugin.py @@ -45,6 +45,7 @@ from reflex_components_core.el.elements.scripts import Noscript, Script from reflex_components_core.el.elements.tables import Col from reflex_components_core.el.elements.typography import Hr +from reflex_components_moment.moment import Moment from reflex_components_radix.themes.layout.box import Box import reflex as rx @@ -1588,6 +1589,60 @@ def test_title_with_stateful_var_child_does_not_wrap_bare_independently() -> Non ) +def test_moment_with_stateful_var_child_does_not_wrap_bare_independently() -> None: + """``rx.moment(state_var)`` must not produce a Bare component child. + + react-moment feeds its children into ``moment()``; a memo-wrapped child + element parses like ``moment({})`` — today at midnight. + """ + moment = Moment.create(STATE_VAR, format="DD.MM.YYYY HH:mm:ss") + ctx, page_ctx = _compile_single_page(lambda: moment) + + assert len(ctx.memoize_wrappers) == 1, ( + "Expected exactly one snapshot wrapper for the moment; got: " + f"{list(ctx.memoize_wrappers)}" + ) + wrapper_tag = next(iter(ctx.memoize_wrappers)) + assert wrapper_tag.lower().startswith("moment_"), ( + f"Wrapper should be derived from Moment, got: {wrapper_tag!r}" + ) + output = page_ctx.output_code + assert output is not None + assert f"jsx({wrapper_tag}," in output, ( + "The page output must call the snapshot wrapper.\n" + f"Page output snippet: {output[:2000]}" + ) + assert "useTestState" not in output, ( + "The state-bearing hook should live inside the memo body, not the page.\n" + f"Page output snippet: {output[:2000]}" + ) + assert "TestState" not in output, ( + "The state-context wiring should live inside the memo body, not the page.\n" + f"Page output snippet: {output[:2000]}" + ) + + +def test_moment_memo_body_renders_text_interpolation_not_bare_component() -> None: + """The moment's memo body must interpolate the state Var as text, not a Bare wrapper.""" + ctx, _page_ctx = _compile_single_page( + lambda: Moment.create(STATE_VAR, format="DD.MM.YYYY HH:mm:ss") + ) + memo_code = _compile_memo_module_text(ctx) + + assert "jsx(Moment" in memo_code, ( + "Moment snapshot body should contain a literal ``jsx(Moment, …)`` " + f"call. Memo code:\n{memo_code[:2000]}" + ) + assert "useTestState" in memo_code, ( + "Moment memo body should carry the stateful hook so the Bare child is " + f"interpolated inline, not lifted out.\nMemo code:\n{memo_code[:2000]}" + ) + assert "Bare_" not in memo_code, ( + "Moment's child must render as a text interpolation, not a Bare " + f"component wrapper.\nMemo code:\n{memo_code[:2000]}" + ) + + def test_meta_with_stateful_var_child_does_not_wrap_bare_independently() -> None: """``rx.el.meta(state_var)`` must not produce a Bare component child.