Skip to content

Single-pass compiler: Make Component effectively immutable #6214

@masenf

Description

@masenf

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:

  1. Adding a _frozen flag that's set after create() completes
  2. Overriding __setattr__ to raise when _frozen is True (with an escape hatch for the construction phase)
  3. 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:

  1. _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.
  2. StatefulComponent.compile_from() — Sets component.children = [...]. With the new memo plugin (ENG-9145), this goes away entirely.
  3. add_meta() in compiler/utils.py — Appends to page.children. Should create a new component instead.
  4. Various __init__ / create() post-processing — These happen during construction and are fine.

Acceptance Criteria

  • Component instances are frozen after create() returns
  • __setattr__ raises AttributeError on frozen components (with clear error message)
  • copy_with(**kwargs) method is available for creating modified copies
  • _add_style_recursive is refactored to return new component instances instead of mutating
  • add_meta() in compiler/utils.py is refactored to not mutate the component
  • All existing tests pass
  • Verify that cached_property works correctly on frozen components (for future use by _structural_hash in ENG-9148)

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementAnything you want improved

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions