Skip to content

refactor: freeze components after create and use copy_with for mutations#6468

Draft
FarhanAliRaza wants to merge 4 commits intoreflex-dev:mainfrom
FarhanAliRaza:immutability
Draft

refactor: freeze components after create and use copy_with for mutations#6468
FarhanAliRaza wants to merge 4 commits intoreflex-dev:mainfrom
FarhanAliRaza:immutability

Conversation

@FarhanAliRaza
Copy link
Copy Markdown
Contributor

Components are now immutable after construction (children stored as tuples, setattr blocks writes outside a small cache allowlist). Compile-time edits go through a new copy_with() helper instead of mutating shared instances, replacing the PageContext.own() page-local clone mechanism so components can be safely reused across pages without deep copies.

All Submissions:

  • Have you followed the guidelines stated in CONTRIBUTING.md file?
  • Have you checked to ensure there aren't any other open Pull Requests for the desired changed?

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

New Feature Submission:

  • Does your submission pass the tests?
  • Have you linted your code locally prior to submission?

Changes To Core Features:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your core changes, as applicable?
  • Have you successfully ran tests with your changes locally?

After these steps, you're ready to open a pull request.

a. Give a descriptive title to your PR.

b. Describe your changes.

c. Put `closes #XXXX` in your comment to auto-close the issue that your PR fixes (if such).

Components are now immutable after construction (children stored as tuples,
__setattr__ blocks writes outside a small cache allowlist). Compile-time
edits go through a new copy_with() helper instead of mutating shared
instances, replacing the PageContext.own() page-local clone mechanism so
components can be safely reused across pages without deep copies.
@FarhanAliRaza FarhanAliRaza requested a review from a team as a code owner May 7, 2026 15:52
# Conflicts:
#	reflex/experimental/memo.py
@FarhanAliRaza FarhanAliRaza marked this pull request as draft May 7, 2026 15:57
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 7, 2026

Greptile Summary

This PR makes Component instances immutable after construction: children is stored as a tuple, a _frozen flag blocks post-create attribute writes, and a new copy_with() helper produces modified frozen copies at compile time. The PageContext.own() page-local clone mechanism is removed in favour of copy_with, and _add_style_recursive is refactored to return a new component instead of mutating in place.

  • Core freeze model (component.py): adds __setattr__ guard, _freeze(), copy_with(), and a _CACHE_ATTRS allowlist for render-path caches; children field changes type from list to tuple.
  • Compile-time mutation sites updated: PageContext.own() is deleted and all 14 call sites across the compiler, plugins, and component modules are migrated to copy_with.
  • TextArea regression: add_style previously removed padding from self.style via pop before returning it under the & textarea selector; now it only reads it, so padding ends up in both the root container style and the nested selector after _add_style_recursive merges self.style.

Confidence Score: 3/5

The freeze contract and copy_with migration are architecturally correct and well-tested, but a TextArea layout regression and a DataTable ref omission need to be addressed before this is safe to merge.

TextArea.add_style missed a load-bearing side-effect of the original pop: padding now appears in both the outer container style and the nested & textarea selector for any TextArea with an explicit padding prop, producing incorrect layout. The DataTable ref omission is narrower but is still a silent behavioral change on an existing render path.

text_area.py needs its add_style migration to preserve the pop-padding semantics; datatable.py should restore the ref injection logic that was lost when switching to the explicit props= path.

Important Files Changed

Filename Overview
packages/reflex-base/src/reflex_base/components/component.py Core freeze/immutability implementation: adds _frozen flag, _freeze(), copy_with(), __setattr__ guard, and refactors _add_style_recursive to return a new component; stale __copy__ docstring flagged
packages/reflex-components-radix/src/reflex_components_radix/themes/components/text_area.py Behavioral regression: add_style no longer pops padding from self.style, causing padding to appear in both the root container style and the & textarea nested selector
packages/reflex-components-gridjs/src/reflex_components_gridjs/datatable.py _render now passes an explicit props dict to super()._render(), bypassing the ref injection logic and silently dropping refs for DataTable components with an id
packages/reflex-base/src/reflex_base/plugins/compiler.py Removes the PageContext.own() clone mechanism; all mutations are now via copy_with() — clean and consistent with the new freeze model
reflex/compiler/utils.py Extracts merge_component_style helper, converts _apply_root_style and add_meta to return new components via copy_with
packages/reflex-components-core/src/reflex_components_core/core/debounce.py Replaces direct mutations with a copy_with call after component creation; semantics preserved
packages/reflex-components-core/src/reflex_components_core/base/bare.py Replaces in-place style propagation through Var._var_data.components with an immutable rebuild returning copy_with(contents=new_contents)
reflex/app.py Replaces functools.reduce + parent.children.append in _build_app_wrappers with an iterative loop using copy_with; ordering preserved
packages/reflex-components-core/src/reflex_components_core/el/elements/forms.py Replaces post-create direct attribute write with copy_with; semantics unchanged
tests/units/components/test_component.py Adds comprehensive freeze/copy_with tests covering mutation rejection, tuple coercion, cache-clearing, and the no-op short-circuit in _add_style_recursive

Comments Outside Diff (2)

  1. packages/reflex-components-radix/src/reflex_components_radix/themes/components/text_area.py, line 107-118 (link)

    P1 Padding duplicated in both root style and & textarea selector

    Before this PR, add_style called self.style.pop("padding") — removing padding from the instance style so it wouldn't be re-merged by _add_style_recursive. Now that components are immutable, add_style only reads padding via .get() and leaves it in self.style. Consequently, _add_style_recursive merges self.style into new_style unconditionally (new_style.update(self.style)), so a component created with padding="1rem" ends up with padding: 1rem in both the root container CSS and inside & textarea { padding: 1rem }. The padding prop is applied twice, producing incorrect layout for any TextArea that sets an explicit padding.

  2. packages/reflex-base/src/reflex_base/components/component.py, line 375-386 (link)

    P2 The docstring still says "suitable for compile-time mutation", but the returned copy is frozen (because vars(self) contains _frozen=True which is propagated). Any caller that reads this docstring and expects a mutable copy will receive an AttributeError on first write.

Reviews (1): Last reviewed commit: "Merge remote-tracking branch 'upstream/m..." | Re-trigger Greptile

Comment on lines +129 to +134
props = {
attr.removesuffix("_"): getattr(self, attr) for attr in self.get_props()
}
props["columns"] = columns
props["data"] = data
return super()._render(props=props)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 ref silently dropped when DataTable has an id

Component._render(props=None) auto-populates ref via self.ref / self.get_ref() only in the props is None branch. DataTable._render now builds the props dict itself and passes it to super()._render(props=props), which takes the else branch and skips that logic entirely. Any DataTable constructed with an explicit id would previously have had a React ref included in the rendered output; that ref is silently absent now.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 7, 2026

Merging this PR will improve performance by 3.68%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
✅ 23 untouched benchmarks
⏩ 2 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
test_get_all_imports_single_pass[_complicated_page] 1.6 ms 1.5 ms +3.68%

Comparing FarhanAliRaza:immutability (78f1602) with main (2df5344)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Bypass __setattr__'s freeze guard during init via vars(self).update(...);
a brand-new instance can't be frozen, so the per-attribute check is pure
overhead. Applied in BaseComponent.__init__, Component.__init__, and
Component._create.

In Component.__init__:
- Lazily allocate event_triggers only when a trigger is actually seen;
  most components have none, so the up-front dict copy was wasted work.
- Single-pass kwargs loop that handles event triggers, var props, and
  unknown on_* errors inline instead of double-iterating.
- Defer the _validate_children imports until after the fast no-op exit.
Use  so an explicitly-passed empty event_triggers dict still
gets copied instead of being dropped. Rename the local  to
 to avoid shadowing the imported  helper.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant