diff --git a/packages/reflex-base/src/reflex_base/components/component.py b/packages/reflex-base/src/reflex_base/components/component.py index 78aca9787f6..14544841662 100644 --- a/packages/reflex-base/src/reflex_base/components/component.py +++ b/packages/reflex-base/src/reflex_base/components/component.py @@ -356,6 +356,12 @@ def __copy__(self) -> BaseComponent: new = self.__class__.__new__(self.__class__) new_dict = vars(new) new_dict.update(vars(self)) + new._clear_compile_caches() + return new + + def _clear_compile_caches(self) -> None: + """Clear cached render/compiler artifacts after compile-time mutation.""" + attrs = cast("dict[str, Any]", vars(self)) for attr in ( "_cached_render_result", "_vars_cache", @@ -363,8 +369,7 @@ def __copy__(self) -> BaseComponent: "_hooks_internal_cache", "_get_component_prop_property", ): - new_dict.pop(attr, None) - return new + attrs.pop(attr, None) def __eq__(self, value: Any) -> bool: """Check if the component is equal to another value. @@ -1367,6 +1372,7 @@ def _add_style_recursive( # Assign the new style self.style = new_style + self._clear_compile_caches() # Recursively add style to the children. for child in self.children: diff --git a/tests/units/compiler/test_memoize_plugin.py b/tests/units/compiler/test_memoize_plugin.py index a23c5c9cc67..c010c58d1fb 100644 --- a/tests/units/compiler/test_memoize_plugin.py +++ b/tests/units/compiler/test_memoize_plugin.py @@ -311,6 +311,46 @@ def special_child() -> Component: assert body_marker not in page_output +def test_foreach_snapshot_memo_applies_component_styles() -> None: + """Foreach memo bodies must render styled children after preview rendering. + + Regression for reflex-dev/reflex#6512: the auto-memo wrapper previews a + Foreach render before the memo body is compiled. If that unstyled preview + render stays cached, default styles from components inside the Foreach + never reach the generated memo module. + """ + from reflex.compiler.compiler import compile_memo_components + + def accordion() -> Component: + return rx.accordion.root( + rx.accordion.item( + header="Click me", + content=rx.text("Content here"), + ), + type="single", + collapsible=True, + width="400px", + ) + + ctx, _page_ctx = _compile_single_page( + lambda: rx.vstack( + rx.foreach(SpecialFormMemoState.items, lambda _item: accordion()), + ) + ) + + memo_files, _memo_imports = compile_memo_components( + components=(), + experimental_memos=tuple(ctx.auto_memo_components.values()), + ) + foreach_code = next( + code for path, code in memo_files if "/Foreach" in path or "\\Foreach" in path + ) + + assert '["width"] : "400px"' in foreach_code + assert '["borderRadius"] : "var(--radius-4)"' in foreach_code + assert '["justifyContent"] : "space-between"' in foreach_code + + def test_foreach_parent_does_not_absorb_sibling_into_snapshot() -> None: """Foreach owns its own snapshot while the parent stays passthrough.