Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
9d053ef
Fix local client state across sibling components
harsh21234i Oct 6, 2026
a633fb9
Keep unrelated memoization for local client state
harsh21234i Oct 6, 2026
8407b4a
Merge branch 'main' into fix/7426-local-client-state-sibling
harsh21234i Oct 14, 2026
bdf01e5
fix: count reused local state component occurrences
harsh21234i Oct 14, 2026
8aacd1f
ci: install asyncio dependencies for legacy playground
harsh21234i Oct 14, 2026
1d89259
fix: preserve shared state scope across styled clones
harsh21234i Oct 14, 2026
a9b46fd
Merge commit '7ccaa4e213fa1f67374002cb28709272866c3d53' into codex/74…
harsh21234i Oct 15, 2026
0199479
fix: include app styles in client state scope scan
harsh21234i Oct 15, 2026
52b64fe
perf: skip unused shared-scope subtree counts
harsh21234i Oct 15, 2026
6f55147
perf: reuse shared-scope prepass counts
harsh21234i Oct 15, 2026
a2572da
test: avoid rounding-sensitive leak assertion
harsh21234i Oct 15, 2026
082f772
Merge branch 'main' into fix/7426-local-client-state-sibling
harsh21234i Oct 17, 2026
9652877
fix: suppress memoization below shared-scope boundaries
harsh21234i Oct 18, 2026
517073c
Merge branch 'main' into fix/7426-local-client-state-sibling
harsh21234i Oct 22, 2026
4aa4d75
refactor: simplify client state scope prepass
harsh21234i Oct 23, 2026
ea54db6
Merge remote-tracking branch 'upstream/main' into codex/7442-conflict
harsh21234i Oct 23, 2026
83062a4
fix: preserve empty component style scopes
harsh21234i Oct 23, 2026
1211cbf
perf: reduce shared scope collection overhead
harsh21234i Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/+7426.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Calling a page-local client state setter from a sibling component no longer raises a `ReferenceError` or leaves the displayed value unchanged.
1 change: 1 addition & 0 deletions packages/reflex-base/news/+7426.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Calling a page-local client state setter from a sibling component no longer raises a `ReferenceError` or leaves the displayed value unchanged.
14 changes: 14 additions & 0 deletions packages/reflex-base/src/reflex_base/plugins/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,14 @@ class PageContext(BaseContext):
# the matching ``leave_component``. Non-empty iff we are inside such a
# subtree.
memoize_suppressor_stack: list[int] = dataclasses.field(default_factory=list)
# Number of components using each page-local hook scope shared across
# multiple components. Memoization boundaries must not split these scopes.
memoize_shared_scope_counts: dict[str, int] = dataclasses.field(
default_factory=dict
)
# Snapshot candidates whose subtree contains only part of a shared local
# hook scope, computed once when the root component is visited.
memoize_shared_scope_split_ids: set[int] = dataclasses.field(default_factory=set)
# Maps both the user-owned original's ``id()`` and the clone's ``id()`` to
# the page-local clone. Lets the walker and plugins rebind children, style,
# or event_triggers on a page-local copy without mutating a user-owned
Expand All @@ -615,6 +623,9 @@ class PageContext(BaseContext):
# garbage collected, and Python may recycle its ``id()`` for a fresh
# component, causing ``own()`` to hand back the wrong clone.
_owned_refs: list[BaseComponent] = dataclasses.field(default_factory=list)
# Original component identity for page-owned clones, used by compiler passes
# whose prepasses run before plugins create those clones.
_owned_original_ids: dict[int, int] = dataclasses.field(default_factory=dict)

def own(self, comp: _BaseComponentT) -> _BaseComponentT:
"""Return a page-local copy of ``comp``, cloning on first encounter.
Expand All @@ -636,6 +647,9 @@ def own(self, comp: _BaseComponentT) -> _BaseComponentT:
self._owned[id(comp)] = new
self._owned[id(new)] = new
self._owned_refs.append(comp)
self._owned_original_ids[id(new)] = self._owned_original_ids.get(
id(comp), id(comp)
)
return new

def merged_imports(self, *, collapse: bool = False) -> ParsedImportDict:
Expand Down
25 changes: 25 additions & 0 deletions packages/reflex-base/src/reflex_base/vars/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,9 @@ class VarData:
default_factory=tuple
)

# Hook scopes that must remain together across every use of the same Var.
shared_scope_keys: tuple[str, ...] = ()

def __init__(
self,
state: str = "",
Expand All @@ -367,6 +370,7 @@ def __init__(
components: Iterable[BaseComponent] | None = None,
app_wraps: Iterable[tuple[int, BaseComponent]] | None = None,
field_dependencies: Mapping[str, Iterable[str]] | None = None,
shared_scope_keys: Iterable[str] | None = None,
):
"""Initialize the var data.

Expand All @@ -387,6 +391,8 @@ def __init__(
and ``field_name`` are the shorthand for a single state with a
single field. Keyword-only in practice: it trails the older
parameters so positional callers of those are unaffected.
shared_scope_keys: Identifiers for hook scopes that must stay
together across every use of this var.
"""
if position is not None:
console.deprecate(
Expand All @@ -413,6 +419,11 @@ def __init__(
object.__setattr__(self, "deps", tuple(deps or []))
object.__setattr__(self, "components", tuple(components or []))
object.__setattr__(self, "app_wraps", tuple(app_wraps or []))
object.__setattr__(
self,
"shared_scope_keys",
tuple(dict.fromkeys(shared_scope_keys or ())),
)

if hooks and any(hooks.values()):
# Merge our dependencies first, so they can be referenced.
Expand All @@ -428,6 +439,11 @@ def __init__(
object.__setattr__(self, "deps", merged_var_data.deps)
object.__setattr__(self, "components", merged_var_data.components)
object.__setattr__(self, "app_wraps", merged_var_data.app_wraps)
object.__setattr__(
self,
"shared_scope_keys",
merged_var_data.shared_scope_keys,
)

@property
def state(self) -> str:
Expand Down Expand Up @@ -532,6 +548,13 @@ def merge(*all: VarData | None) -> VarData | None:
app_wraps=tuple(
(priority, wrapper) for (priority, _tag), wrapper in app_wraps.items()
),
shared_scope_keys=tuple(
dict.fromkeys(
key
for var_data in all_var_datas
for key in var_data.shared_scope_keys
)
),
)

def __bool__(self) -> bool:
Expand All @@ -547,6 +570,7 @@ def __bool__(self) -> bool:
or self.deps
or self.components
or self.app_wraps
or self.shared_scope_keys
)

@functools.cached_property
Expand Down Expand Up @@ -577,6 +601,7 @@ def _identity_key(self) -> tuple:
(priority, component.tag or type(component).__name__)
for priority, component in self.app_wraps
),
self.shared_scope_keys,
)

def __eq__(self, other: object) -> bool:
Expand Down
2 changes: 1 addition & 1 deletion reflex/compiler/plugins/builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ def default_page_plugins(
chain: list[Plugin] = [*plugins, DefaultPagePlugin()]
if style is not None:
chain.append(ApplyStylePlugin(style=style))
chain.extend((DefaultCollectorPlugin(), MemoizeStatefulPlugin()))
chain.extend((DefaultCollectorPlugin(), MemoizeStatefulPlugin(style=style)))
return tuple(chain)


Expand Down
Loading
Loading