Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
File renamed without changes.
1 change: 1 addition & 0 deletions news/6841.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Avoid leaking temporary router dirtiness into linked/shared state event deltas.
12 changes: 11 additions & 1 deletion reflex/istate/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,21 @@ 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)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
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()
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
Expand Down
76 changes: 76 additions & 0 deletions tests/units/test_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,82 @@ 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."""

value: int = 0


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_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,
Expand Down
Loading