From 915dd921c5668b82b2af040e48684a1a2b2dc82c Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Tue, 4 Aug 2026 16:28:48 +0530 Subject: [PATCH 1/3] Avoid root dirty delta for shared state events --- news/6392.bugfix.md | 1 + reflex/istate/shared.py | 8 ++++++++ tests/units/test_state.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 news/6392.bugfix.md diff --git a/news/6392.bugfix.md b/news/6392.bugfix.md new file mode 100644 index 00000000000..755e5824602 --- /dev/null +++ b/news/6392.bugfix.md @@ -0,0 +1 @@ +Avoid leaking temporary router dirtiness into linked/shared state event deltas. diff --git a/reflex/istate/shared.py b/reflex/istate/shared.py index 41b1f519cd3..dc95808b421 100644 --- a/reflex/istate/shared.py +++ b/reflex/istate/shared.py @@ -102,11 +102,19 @@ async def _patch_state( linked_state.dirty_vars.update(linked_state.computed_vars) linked_state._mark_dirty() # Apply the updates into the existing state tree for rehydrate. + # For regular linked-state events this router dirtiness is temporary: + # it forces router-dependent computed vars to resolve for the patched + # tree, but should not leak into the event's final delta. root_state = original_state._get_root_state() + root_dirty_vars = set(root_state.dirty_vars) + root_dirty_substates = set(root_state.dirty_substates) root_state.dirty_vars.add("router") root_state.dirty_vars.add(ROUTER_DATA) root_state._mark_dirty() await root_state._get_resolved_delta() + if not full_delta: + root_state.dirty_vars = root_dirty_vars + root_state.dirty_substates = root_dirty_substates yield finally: original_parent_state.substates[state_name] = original_state diff --git a/tests/units/test_state.py b/tests/units/test_state.py index 7106287a43d..7f965bac4ac 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -922,6 +922,41 @@ def test_setting_inherited_backend_var_does_not_mark_child_touched( assert not child_touched +class _LinkedStatePatchRoot(BaseState): + """Root state for testing linked-state dirty propagation.""" + + +class _LinkedStatePatchShared(_LinkedStatePatchRoot): + """Substate used to exercise _patch_state without full SharedState setup.""" + + counter: int = 0 + + +@pytest.mark.asyncio +async def test_linked_state_event_does_not_dirty_root_state(): + """Linked-state events should not leak temporary router dirtiness.""" + from reflex.istate.shared import _patch_state + + private_tree = _LinkedStatePatchRoot() + linked_tree = _LinkedStatePatchRoot() + + shared_state_name = _LinkedStatePatchShared.get_name() + private_state = private_tree.substates[shared_state_name] + linked_state = linked_tree.substates[shared_state_name] + + assert isinstance(private_state, _LinkedStatePatchShared) + assert isinstance(linked_state, _LinkedStatePatchShared) + + private_tree._clean() + + async with _patch_state(private_state, linked_state, full_delta=False): + linked_state.counter = 1 + + assert "router" not in private_tree.dirty_vars + assert constants.ROUTER_DATA not in private_tree.dirty_vars + assert private_tree.get_full_name() not in private_tree.get_delta() + + @pytest.mark.asyncio async def test_process_event_simple( token: str, From 63b081b12bcb20e8013c87f5a9aa80ebf9573bd1 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Tue, 4 Aug 2026 16:55:16 +0530 Subject: [PATCH 2/3] Handle linked state patch resolve failures --- news/{6392.bugfix.md => 6841.bugfix.md} | 0 reflex/istate/shared.py | 10 +++--- tests/units/test_state.py | 41 +++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 4 deletions(-) rename news/{6392.bugfix.md => 6841.bugfix.md} (100%) diff --git a/news/6392.bugfix.md b/news/6841.bugfix.md similarity index 100% rename from news/6392.bugfix.md rename to news/6841.bugfix.md diff --git a/reflex/istate/shared.py b/reflex/istate/shared.py index dc95808b421..47d12976ebb 100644 --- a/reflex/istate/shared.py +++ b/reflex/istate/shared.py @@ -111,10 +111,12 @@ async def _patch_state( root_state.dirty_vars.add("router") root_state.dirty_vars.add(ROUTER_DATA) root_state._mark_dirty() - await root_state._get_resolved_delta() - if not full_delta: - root_state.dirty_vars = root_dirty_vars - root_state.dirty_substates = root_dirty_substates + try: + await root_state._get_resolved_delta() + finally: + if not full_delta: + root_state.dirty_vars = root_dirty_vars + root_state.dirty_substates = root_dirty_substates yield finally: original_parent_state.substates[state_name] = original_state diff --git a/tests/units/test_state.py b/tests/units/test_state.py index 7f965bac4ac..83caf03b3cc 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -925,6 +925,8 @@ def test_setting_inherited_backend_var_does_not_mark_child_touched( class _LinkedStatePatchRoot(BaseState): """Root state for testing linked-state dirty propagation.""" + value: int = 0 + class _LinkedStatePatchShared(_LinkedStatePatchRoot): """Substate used to exercise _patch_state without full SharedState setup.""" @@ -957,6 +959,45 @@ async def test_linked_state_event_does_not_dirty_root_state(): assert private_tree.get_full_name() not in private_tree.get_delta() +@pytest.mark.asyncio +async def test_linked_state_patch_restores_root_dirty_state_on_resolve_error( + monkeypatch: pytest.MonkeyPatch, +): + """Temporary root dirtiness should be cleaned if delta resolution fails.""" + from reflex.istate.shared import _patch_state + + private_tree = _LinkedStatePatchRoot() + linked_tree = _LinkedStatePatchRoot() + + shared_state_name = _LinkedStatePatchShared.get_name() + private_state = private_tree.substates[shared_state_name] + linked_state = linked_tree.substates[shared_state_name] + + assert isinstance(private_state, _LinkedStatePatchShared) + assert isinstance(linked_state, _LinkedStatePatchShared) + + private_tree.value = 1 + private_tree.dirty_substates.add("existing") + original_dirty_vars = set(private_tree.dirty_vars) + original_dirty_substates = set(private_tree.dirty_substates) + + async def raise_resolve_error(): + await asyncio.sleep(0) + msg = "delta resolution failed" + raise RuntimeError(msg) + + object.__setattr__(private_tree, "_get_resolved_delta", raise_resolve_error) + + with pytest.raises(RuntimeError, match="delta resolution failed"): + async with _patch_state(private_state, linked_state, full_delta=False): + pass + + assert private_tree.dirty_vars == original_dirty_vars + assert private_tree.dirty_substates == original_dirty_substates + assert private_tree.substates[shared_state_name] is private_state + assert linked_state.parent_state is linked_tree + + @pytest.mark.asyncio async def test_process_event_simple( token: str, From c19e5a2c8389ea87dc6c5282b2a2571ff89f8142 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Tue, 4 Aug 2026 17:01:49 +0530 Subject: [PATCH 3/3] Use valid news fragment type for Pillow pin --- news/{6836.chore.md => 6836.misc.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename news/{6836.chore.md => 6836.misc.md} (100%) diff --git a/news/6836.chore.md b/news/6836.misc.md similarity index 100% rename from news/6836.chore.md rename to news/6836.misc.md