88from typing import Any , cast
99
1010import pytest
11- from reflex_base .components .component import Component , field
11+ from reflex_base .components .component import Component
12+ from reflex_base .components .component import field as component_field
1213from reflex_base .components .memoize_helpers import (
1314 MemoizationStrategy ,
1415 get_memoization_strategy ,
1516)
1617from reflex_base .constants .compiler import MemoizationDisposition , MemoizationMode
1718from reflex_base .plugins import CompileContext , CompilerHooks , PageContext
1819from reflex_base .vars import VarData
19- from reflex_base .vars .base import LiteralVar , Var
20+ from reflex_base .vars .base import Field , LiteralVar , Var , field
2021from reflex_components_core .base .bare import Bare
2122from reflex_components_core .base .fragment import Fragment
2223from reflex_components_core .base .link import RawLink , ScriptTag
24+ from reflex_components_core .core .foreach import Foreach
2325from reflex_components_core .el .elements .forms import BaseInput , Textarea
2426from reflex_components_core .el .elements .inline import Br , Wbr
2527from reflex_components_core .el .elements .media import (
3739from reflex_components_core .el .elements .scripts import Noscript , Script
3840from reflex_components_core .el .elements .tables import Col
3941from reflex_components_core .el .elements .typography import Hr
42+ from reflex_components_radix .themes .layout .box import Box
4043
4144import reflex as rx
4245import reflex .compiler .plugins .memoize as memoize_plugin
4346from reflex .compiler .plugins import DefaultCollectorPlugin , default_page_plugins
4447from reflex .compiler .plugins .memoize import MemoizeStatefulPlugin , _should_memoize
4548from reflex .experimental .memo import (
4649 ExperimentalMemoComponent ,
50+ ExperimentalMemoComponentDefinition ,
4751 create_passthrough_component_memo ,
4852)
4953from reflex .state import BaseState
@@ -62,7 +66,7 @@ class WithProp(Component):
6266 tag = "WithProp"
6367 library = "with-prop-lib"
6468
65- label : Var [str ] = field (default = LiteralVar .create ("" ))
69+ label : Var [str ] = component_field (default = LiteralVar .create ("" ))
6670
6771
6872class LeafComponent (Component ):
@@ -72,9 +76,9 @@ class LeafComponent(Component):
7276
7377
7478class SpecialFormMemoState (BaseState ):
75- items : list [str ] = ["a" ]
76- flag : bool = True
77- value : str = "a"
79+ items : Field [ list [str ]] = field ( default_factory = lambda : ["a" ])
80+ flag : Field [ bool ] = field ( default = True )
81+ value : Field [ str ] = field ( default = "a" )
7882
7983
8084@dataclasses .dataclass (slots = True )
@@ -251,9 +255,61 @@ def special_child() -> Component:
251255 assert body_marker not in page_output
252256
253257
258+ def test_foreach_parent_does_not_absorb_sibling_into_snapshot () -> None :
259+ """Foreach owns its own snapshot while the parent stays passthrough.
260+
261+ Regression for the foreach-parent memoization fix: a parent component
262+ holding a Foreach used to be promoted to SNAPSHOT, absorbing any sibling
263+ reactive content into the same wide memo body. The parent should now render
264+ on the page side, with Foreach and any reactive sibling each getting their
265+ own independent wrapper.
266+ """
267+ ctx , _page_ctx = _compile_single_page (
268+ lambda : rx .box (
269+ Bare .create (SpecialFormMemoState .items .length ()),
270+ rx .foreach (
271+ SpecialFormMemoState .items ,
272+ lambda item : rx .text (item ),
273+ ),
274+ )
275+ )
276+
277+ wrapped_definitions = [
278+ definition
279+ for definition in ctx .auto_memo_components .values ()
280+ if isinstance (definition , ExperimentalMemoComponentDefinition )
281+ ]
282+ wrapped_types = {type (definition .component ) for definition in wrapped_definitions }
283+
284+ assert len (wrapped_definitions ) == 2
285+ assert Box not in wrapped_types
286+
287+ foreach_definition = next (
288+ definition
289+ for definition in wrapped_definitions
290+ if isinstance (definition .component , Foreach )
291+ )
292+ assert (
293+ get_memoization_strategy (foreach_definition .component )
294+ is MemoizationStrategy .SNAPSHOT
295+ )
296+
297+ bare_definition = next (
298+ definition
299+ for definition in wrapped_definitions
300+ if isinstance (definition .component , Bare )
301+ )
302+ assert (
303+ get_memoization_strategy (bare_definition .component )
304+ is MemoizationStrategy .PASSTHROUGH
305+ )
306+ assert bare_definition is not foreach_definition
307+
308+
254309def test_common_memoization_snapshot_helper_classifies_snapshot_cases () -> None :
255310 """The shared memoization strategy classifies structural render forms."""
256311 from reflex_components_core .core .cond import Cond
312+ from reflex_components_core .core .foreach import Foreach
257313 from reflex_components_core .core .match import Match
258314 from reflex_components_core .el .elements .forms import Form , Input
259315
@@ -280,7 +336,13 @@ def test_common_memoization_snapshot_helper_classifies_snapshot_cases() -> None:
280336 ),
281337 )
282338
283- assert get_memoization_strategy (foreach_parent ) is MemoizationStrategy .SNAPSHOT
339+ # A parent with a structural-memoization child (Foreach) is itself
340+ # PASSTHROUGH — the snapshotting is owned by the structural child, which
341+ # captures its whole subtree.
342+ assert get_memoization_strategy (foreach_parent ) is MemoizationStrategy .PASSTHROUGH
343+ foreach_child = foreach_parent .children [0 ]
344+ assert isinstance (foreach_child , Foreach )
345+ assert get_memoization_strategy (foreach_child ) is MemoizationStrategy .SNAPSHOT
284346 assert get_memoization_strategy (cond_fragment ) is MemoizationStrategy .PASSTHROUGH
285347 # Cond and Match now use passthrough so branch JSX renders on the page side
286348 # and the memo body just selects via children[i] indexing.
0 commit comments