Summary
Make Component an effectively immutable type after construction. Immutability is a correctness and simplicity win — it eliminates a class of bugs caused by post-creation mutation and makes the component tree easier to reason about during compilation. It also enables caching opportunities (see below), though the primary caching mechanism for cross-reload use will be structural hashing (ENG-9148), not id()-based identity.
Related to ENG-9142, ENG-9143, ENG-9145, ENG-9148.
Background
Today, Component instances are mutable. After Component.create(), various passes modify the component in place:
_add_style_recursive() mutates self.style on every component in the tree
StatefulComponent.compile_from() replaces component.children with memoized versions
- Various compiler phases read and write component attributes
This mutability means the compiler can't make assumptions about whether a component has been modified between phases, and it complicates any caching strategy.
Proposed Approach
Frozen-after-construction pattern
After Component.create() returns, the component should be treated as immutable. Any "mutation" creates a new component instance (copy-on-write):
# Instead of:
component.style = new_style # mutation!
# Do:
component = component.copy_with(style=new_style) # returns new instance
This can be enforced by:
- Adding a
_frozen flag that's set after create() completes
- Overriding
__setattr__ to raise when _frozen is True (with an escape hatch for the construction phase)
- Providing a
copy_with(**kwargs) method that creates a shallow copy with updated fields
Caching benefits
Immutability enables two levels of caching:
Within a single compilation run, id()-based caching is useful when the exact same component object appears in multiple places (e.g., a shared layout component stored in a module-level variable). Since it's frozen, its id() is a stable cache key for the duration of the run.
Across hot reloads, id()-based caching is not sufficient — common patterns like helper functions (navbar(), sidebar()) recreate structurally identical component trees with new id() values on every call. Cross-reload caching requires structural hashing as described in ENG-9148. Immutability supports this by making _structural_hash a reliable cached_property — once computed, it never needs recomputation since the component can't change.
Impact on existing code
The main places that currently mutate components post-creation:
_add_style_recursive() — Must become a copy-on-write operation. The ApplyStylePlugin would return new component instances rather than mutating in place. This is the biggest change.
StatefulComponent.compile_from() — Sets component.children = [...]. With the new memo plugin (ENG-9145), this goes away entirely.
add_meta() in compiler/utils.py — Appends to page.children. Should create a new component instead.
- Various
__init__ / create() post-processing — These happen during construction and are fine.
Acceptance Criteria
Key Files
reflex/components/component.py:
Component class — needs frozen flag and __setattr__ override
BaseComponent.create() — set frozen flag after construction
_add_style_recursive() (~line 1251) — needs copy-on-write refactor
reflex/compiler/utils.py:
add_meta() (~line 417) — mutates page.children
reflex/compiler/compiler.py:
compile_unevaluated_page() — calls _add_style_recursive and add_meta
Notes
- The frozen pattern is inspired by Python's
dataclasses(frozen=True) but needs to be more flexible since Component uses Pydantic-style field definitions.
- Consider using
__slots__ where possible for memory efficiency, though this may conflict with Pydantic's model system.
- Immutability is a prerequisite for the structural hashing in ENG-9148 —
_structural_hash as a cached_property only works if the component's type, props, and children can never change after construction.
- This issue focuses on making components immutable and refactoring mutation sites. The caching strategies themselves (both
id()-based within a run and structural-hash-based across reloads) are covered in ENG-9148.
Summary
Make
Componentan effectively immutable type after construction. Immutability is a correctness and simplicity win — it eliminates a class of bugs caused by post-creation mutation and makes the component tree easier to reason about during compilation. It also enables caching opportunities (see below), though the primary caching mechanism for cross-reload use will be structural hashing (ENG-9148), notid()-based identity.Related to ENG-9142, ENG-9143, ENG-9145, ENG-9148.
Background
Today,
Componentinstances are mutable. AfterComponent.create(), various passes modify the component in place:_add_style_recursive()mutatesself.styleon every component in the treeStatefulComponent.compile_from()replacescomponent.childrenwith memoized versionsThis mutability means the compiler can't make assumptions about whether a component has been modified between phases, and it complicates any caching strategy.
Proposed Approach
Frozen-after-construction pattern
After
Component.create()returns, the component should be treated as immutable. Any "mutation" creates a new component instance (copy-on-write):This can be enforced by:
_frozenflag that's set aftercreate()completes__setattr__to raise when_frozenis True (with an escape hatch for the construction phase)copy_with(**kwargs)method that creates a shallow copy with updated fieldsCaching benefits
Immutability enables two levels of caching:
Within a single compilation run,
id()-based caching is useful when the exact same component object appears in multiple places (e.g., a shared layout component stored in a module-level variable). Since it's frozen, itsid()is a stable cache key for the duration of the run.Across hot reloads,
id()-based caching is not sufficient — common patterns like helper functions (navbar(),sidebar()) recreate structurally identical component trees with newid()values on every call. Cross-reload caching requires structural hashing as described in ENG-9148. Immutability supports this by making_structural_hasha reliablecached_property— once computed, it never needs recomputation since the component can't change.Impact on existing code
The main places that currently mutate components post-creation:
_add_style_recursive()— Must become a copy-on-write operation. TheApplyStylePluginwould return new component instances rather than mutating in place. This is the biggest change.StatefulComponent.compile_from()— Setscomponent.children = [...]. With the new memo plugin (ENG-9145), this goes away entirely.add_meta()incompiler/utils.py— Appends topage.children. Should create a new component instead.__init__/create()post-processing — These happen during construction and are fine.Acceptance Criteria
Componentinstances are frozen aftercreate()returns__setattr__raisesAttributeErroron frozen components (with clear error message)copy_with(**kwargs)method is available for creating modified copies_add_style_recursiveis refactored to return new component instances instead of mutatingadd_meta()incompiler/utils.pyis refactored to not mutate the componentcached_propertyworks correctly on frozen components (for future use by_structural_hashin ENG-9148)Key Files
reflex/components/component.py:Componentclass — needs frozen flag and__setattr__overrideBaseComponent.create()— set frozen flag after construction_add_style_recursive()(~line 1251) — needs copy-on-write refactorreflex/compiler/utils.py:add_meta()(~line 417) — mutatespage.childrenreflex/compiler/compiler.py:compile_unevaluated_page()— calls_add_style_recursiveandadd_metaNotes
dataclasses(frozen=True)but needs to be more flexible sinceComponentuses Pydantic-style field definitions.__slots__where possible for memory efficiency, though this may conflict with Pydantic's model system._structural_hashas acached_propertyonly works if the component's type, props, and children can never change after construction.id()-based within a run and structural-hash-based across reloads) are covered in ENG-9148.