1818from typing import TYPE_CHECKING , Any , ClassVar , TypeVar , cast , get_args , get_origin
1919
2020from rich .markup import escape
21- from typing_extensions import dataclass_transform
21+ from typing_extensions import Self , dataclass_transform
2222
2323from reflex_base import constants
2424from reflex_base .breakpoints import Breakpoints
@@ -266,9 +266,20 @@ class BaseComponent(metaclass=BaseComponentMeta):
266266 This is something that can be rendered as a Component via the Reflex compiler.
267267 """
268268
269- children : list [BaseComponent ] = field (
269+ _frozen : ClassVar [bool ] = False
270+
271+ # Render-path caches; allowed to be written even on frozen instances.
272+ _CACHE_ATTRS : ClassVar [frozenset [str ]] = frozenset ({
273+ "_cached_render_result" ,
274+ "_vars_cache" ,
275+ "_imports_cache" ,
276+ "_hooks_internal_cache" ,
277+ "_get_component_prop_property" ,
278+ })
279+
280+ children : tuple [BaseComponent , ...] = field (
270281 doc = "The children nested within the component." ,
271- default_factory = list ,
282+ default_factory = tuple ,
272283 is_javascript_property = False ,
273284 )
274285
@@ -293,24 +304,73 @@ def __init__(
293304 Args:
294305 **kwargs: The kwargs to pass to the component.
295306 """
307+ if "children" in kwargs :
308+ kwargs ["children" ] = tuple (kwargs ["children" ])
296309 for key , value in kwargs .items ():
297310 setattr (self , key , value )
298311 for name , value in self .get_fields ().items ():
299312 if name not in kwargs :
300313 setattr (self , name , value .default_value ())
301314
315+ def __setattr__ (self , key : str , value : Any ) -> None :
316+ """Block writes to frozen components, except for cache attributes.
317+
318+ Args:
319+ key: The attribute name.
320+ value: The attribute value.
321+
322+ Raises:
323+ AttributeError: If the component is frozen and the attribute is not a cache.
324+ """
325+ if self .__dict__ .get ("_frozen" , False ) and key not in type (self )._CACHE_ATTRS :
326+ msg = (
327+ f"Cannot set { key !r} on frozen { type (self ).__name__ } ; "
328+ "use copy_with() to create a modified copy."
329+ )
330+ raise AttributeError (msg )
331+ super ().__setattr__ (key , value )
332+
333+ def _freeze (self ) -> None :
334+ """Mark this component as frozen.
335+
336+ Subsequent attribute writes outside the cache allowlist will raise.
337+ """
338+ object .__setattr__ (self , "_frozen" , True )
339+
340+ def copy_with (self , ** updates : Any ) -> Self :
341+ """Return a frozen shallow copy with updated fields.
342+
343+ Bypasses ``__setattr__`` for speed and to skip the freeze guard.
344+ Render-path caches are dropped because they may depend on the fields
345+ being replaced.
346+
347+ Args:
348+ **updates: Field values to override on the copy.
349+
350+ Returns:
351+ A new frozen instance with the requested updates applied.
352+ """
353+ new = self .__class__ .__new__ (self .__class__ )
354+ d = vars (new )
355+ d .update (vars (self ))
356+ for cache_attr in type (self )._CACHE_ATTRS :
357+ d .pop (cache_attr , None )
358+ if "children" in updates :
359+ updates ["children" ] = tuple (updates ["children" ])
360+ d .update (updates )
361+ d ["_frozen" ] = True
362+ return new
363+
302364 def set (self , ** kwargs ):
303- """Set the component props.
365+ """Set the component props, returning a new frozen instance .
304366
305367 Args:
306368 **kwargs: The kwargs to set.
307369
308370 Returns:
309- The component with the updated props.
371+ A new component with the updated props.
310372 """
311- for key , value in kwargs .items ():
312- setattr (self , key , value )
313- return self
373+ return self .copy_with (** kwargs )
314374
315375 def __copy__ (self ) -> BaseComponent :
316376 """Return a shallow copy suitable for compile-time mutation.
@@ -327,13 +387,7 @@ def __copy__(self) -> BaseComponent:
327387 new = self .__class__ .__new__ (self .__class__ )
328388 new_dict = vars (new )
329389 new_dict .update (vars (self ))
330- for attr in (
331- "_cached_render_result" ,
332- "_vars_cache" ,
333- "_imports_cache" ,
334- "_hooks_internal_cache" ,
335- "_get_component_prop_property" ,
336- ):
390+ for attr in type (self )._CACHE_ATTRS :
337391 new_dict .pop (attr , None )
338392 return new
339393
@@ -1223,9 +1277,11 @@ def _create(cls: type[T], children: Sequence[BaseComponent], **props: Any) -> T:
12231277 Returns:
12241278 The component.
12251279 """
1280+ children_tuple = tuple (children )
12261281 comp = cls .__new__ (cls )
1227- super (Component , comp ).__init__ (id = props .get ("id" ), children = list (children ))
1228- comp ._post_init (children = list (children ), ** props )
1282+ super (Component , comp ).__init__ (id = props .get ("id" ), children = children_tuple )
1283+ comp ._post_init (children = children_tuple , ** props )
1284+ comp ._freeze ()
12291285 return comp
12301286
12311287 @classmethod
@@ -1241,10 +1297,12 @@ def _unsafe_create(
12411297 Returns:
12421298 The component.
12431299 """
1300+ children_tuple = tuple (children )
12441301 comp = cls .__new__ (cls )
1245- super (Component , comp ).__init__ (id = props .get ("id" ), children = list ( children ) )
1302+ super (Component , comp ).__init__ (id = props .get ("id" ), children = children_tuple )
12461303 for prop , value in props .items ():
12471304 setattr (comp , prop , value )
1305+ comp ._freeze ()
12481306 return comp
12491307
12501308 def add_style (self ) -> dict [str , Any ] | None :
@@ -1311,40 +1369,47 @@ def _add_style_recursive(
13111369 theme: The theme to apply. (for retro-compatibility with deprecated _apply_theme API)
13121370
13131371 Returns:
1314- The component with the additional style.
1372+ A component with the additional style; ``self`` if nothing changed .
13151373
13161374 Raises:
13171375 UserWarning: If `_add_style` has been overridden.
13181376 """
1319- # 1. Default style from `_add_style`/`add_style`.
13201377 if type (self )._add_style != Component ._add_style :
13211378 msg = "Do not override _add_style directly. Use add_style instead."
13221379 raise UserWarning (msg )
1323- new_style = self ._add_style ()
1324- style_vars = [new_style ._var_data ]
13251380
1326- # 2. User-defined style from `App.style`.
1381+ style_addition = self . _add_style ()
13271382 component_style = self ._get_component_style (style )
1328- if component_style :
1329- new_style .update (component_style )
1330- style_vars .append (component_style ._var_data )
1331-
1332- # 4. style dict and css props passed to the component instance.
1333- new_style .update (self .style )
1334- style_vars .append (self .style ._var_data )
1335-
1336- new_style ._var_data = VarData .merge (* style_vars )
1337-
1338- # Assign the new style
1339- self .style = new_style
1383+ has_style_change = bool (style_addition ) or bool (component_style )
13401384
1341- # Recursively add style to the children.
1342- for child in self .children :
1343- # Skip non-Component children.
1385+ new_children : list | None = None
1386+ for i , child in enumerate (self .children ):
13441387 if not isinstance (child , Component ):
13451388 continue
1346- child ._add_style_recursive (style , theme )
1347- return self
1389+ updated = child ._add_style_recursive (style , theme )
1390+ if updated is child :
1391+ continue
1392+ if new_children is None :
1393+ new_children = list (self .children )
1394+ new_children [i ] = updated
1395+
1396+ if not has_style_change and new_children is None :
1397+ return self
1398+
1399+ updates : dict [str , Any ] = {}
1400+ if has_style_change :
1401+ new_style = style_addition
1402+ style_vars = [new_style ._var_data ]
1403+ if component_style :
1404+ new_style .update (component_style )
1405+ style_vars .append (component_style ._var_data )
1406+ new_style .update (self .style )
1407+ style_vars .append (self .style ._var_data )
1408+ new_style ._var_data = VarData .merge (* style_vars )
1409+ updates ["style" ] = new_style
1410+ if new_children is not None :
1411+ updates ["children" ] = tuple (new_children )
1412+ return self .copy_with (** updates )
13481413
13491414 def _get_style (self ) -> dict :
13501415 """Get the style for the component.
@@ -2342,8 +2407,7 @@ def get_component(self) -> Component:
23422407 except Exception :
23432408 style = {}
23442409
2345- component ._add_style_recursive (style )
2346- return component
2410+ return component ._add_style_recursive (style )
23472411
23482412 def _get_all_app_wrap_components (
23492413 self , * , ignore_ids : set [int ] | None = None
0 commit comments