Skip to content

Conversation

@masenf
Copy link
Collaborator

@masenf masenf commented Dec 6, 2025

It works by defining a substate of SharedState and then calling self._link_to(target_token) from some event handler. from that point on, whenever that user's state is loaded, the StateManager will patch in the linked shared states. whenever a linked state is modified, we explicitly load all of the other linked tokens, patch in the modified states, and send a delta to those clients

You can call ._unlink to remove the link association, which causes the substate to be subsequently loaded from the client_token's tree as a private state

It is intended to work transparently with computed vars, background events, and frontend rendering.

Test code

import reflex as rx


class MySharedThing(rx.SharedState):
    my_counter: int = 0

    @rx.event
    async def toggle_link(self):
        if not self._linked_to:
            await self._link_to(await self.get_var_value(State.shared_token))
        return await self._unlink()

    @rx.event
    def increment(self):
        self.my_counter += 1

    @rx.event
    def decrement(self):
        self.my_counter -= 1

    @rx.var
    def linked_to(self) -> str:
        return self._linked_to or "not linked"

    @rx.var
    def linked_from(self) -> str:
        return ", ".join(self._linked_from) or "no links"

    @rx.event(background=True)
    async def delayed_multi_increment(self, amount: int):
        import asyncio

        for _ in range(amount):
            await asyncio.sleep(1)
            async with self:
                self.my_counter += 1


class State(rx.State):
    @rx.var
    def shared_token(self) -> str:
        return (self.room or "shared_global").replace("_", "-")

    @rx.var
    async def current_count(self) -> int:
        shared_state = await self.get_state(MySharedThing)
        return shared_state.my_counter

    @rx.event
    async def print_current_count(self):
        shared_state = await self.get_state(MySharedThing)
        print(f"Current count is: {shared_state.my_counter}")


def index() -> rx.Component:
    return rx.container(
        rx.color_mode.button(position="top-right"),
        rx.vstack(
            rx.text(f"Shared token: {State.shared_token}"),
            rx.button(f"Linked To: {MySharedThing.linked_to}", on_click=MySharedThing.toggle_link),
            rx.text(f"Linked From: {MySharedThing.linked_from}"),
            rx.heading(State.current_count),
            rx.button(
                "Increment",
                on_click=MySharedThing.increment,
            ),
            rx.button(
                "Increment 5 times with 1s delay",
                on_click=MySharedThing.delayed_multi_increment(5),
            ),
            rx.button(
                "Decrement",
                on_click=MySharedThing.decrement,
            ),
            rx.button(
                "Print Current Count to Console",
                on_click=State.print_current_count,
            ),
        ),
    )


app = rx.App()
app.add_page(index, route="/[room]")
app.add_page(index)

Access /my-room-id to enter separate "rooms" for arbitrary sharing domains. This allows any number of clients to share state.

It works by defining a substate of SharedState and then calling
self._link_to(target_token) from some event handler.  from that point on,
whenever that user's state is loaded, the StateManager will patch in the linked
shared states.  whenever a linked state is modified, we explicitly load all of
the other linked tokens, patch in the modified states, and send a delta to
those clients

You can call ._unlink to remove the link association, which causes the substate
to be subsequently loaded from the client_token's tree as a private state

It is intended to work transparently with computed vars, background events, and
frontend rendering.
@linear
Copy link

linear bot commented Dec 6, 2025

@codspeed-hq
Copy link

codspeed-hq bot commented Dec 6, 2025

CodSpeed Performance Report

Merging #6024 will not alter performance

Comparing masenf/linked-state (e99f462) with main (805ad97)

Summary

✅ 8 untouched

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Dec 6, 2025

Greptile Overview

Greptile Summary

This PR implements SharedState, a new API that allows multiple clients to share state by linking their states to a common token. When a client calls _link_to(target_token), their state becomes synchronized with the target token's state, and changes propagate to all linked clients.

Key Changes

  • New SharedState base class in reflex/istate/shared.py with _link_to() and _unlink() methods
  • StateManager.modify_state_with_links() automatically fetches and patches linked states into the current state tree
  • When linked states are modified, changes propagate to all other linked clients through recursive app.modify_state() calls
  • Added _override_base_method decorator to allow SharedState to override base methods without warnings

Critical Issue Found

  • Type mismatch: previous_dirty_vars parameter has inconsistent type signatures across the call chain (set[str] | None in reflex/app.py:1568 and reflex/istate/manager/__init__.py:121, but dict[str, set[str]] | None expected by _modify_linked_states in reflex/istate/shared.py:144). This will cause runtime errors when changes propagate between linked clients.

Confidence Score: 1/5

  • This PR has a critical type mismatch that will cause runtime errors during state propagation
  • The type signature mismatch for previous_dirty_vars will cause an AttributeError when the code attempts to call .get() on what it expects to be a dict but is actually a set (or None). This breaks the core functionality of propagating changes between linked clients.
  • Pay close attention to reflex/app.py and reflex/istate/manager/__init__.py - the type signatures for previous_dirty_vars must be corrected to dict[str, set[str]] | None

Important Files Changed

File Analysis

Filename Score Overview
reflex/istate/shared.py 2/5 New SharedState implementation for linking states across clients - contains type signature issue in _modify_linked_states that causes parameter type mismatch
reflex/app.py 2/5 Added previous_dirty_vars parameter to modify_state with incorrect type (set[str] instead of dict[str, set[str]])
reflex/istate/manager/init.py 2/5 Added modify_state_with_links method with incorrect previous_dirty_vars type signature
reflex/state.py 4/5 Added _override_base_method decorator to allow SharedState to override base methods without triggering warnings

Sequence Diagram

sequenceDiagram
    participant Client1
    participant App
    participant StateManager
    participant SharedState1
    participant LinkedToken
    participant SharedState2
    participant Client2

    Note over Client1,Client2: Linking Process
    Client1->>App: Event Handler calls _link_to(token)
    App->>StateManager: modify_state_with_links(client1_token)
    StateManager->>SharedState1: _modify_linked_states()
    SharedState1->>StateManager: modify_state(linked_token)
    StateManager->>LinkedToken: Lock and fetch state
    LinkedToken-->>SharedState1: Return linked state
    SharedState1->>SharedState1: Patch linked state into tree
    SharedState1->>SharedState2: Update _linked_from set
    SharedState1-->>Client1: Return hydrate events

    Note over Client1,Client2: Propagation on Modification
    Client1->>App: Modify shared state
    App->>StateManager: modify_state_with_links(client1_token)
    StateManager->>SharedState1: _modify_linked_states()
    SharedState1->>StateManager: Lock linked_token states
    SharedState1->>SharedState1: Apply changes to linked state
    SharedState1->>SharedState1: Collect dirty_vars & affected_tokens
    SharedState1->>App: Call app.modify_state for each affected_token
    App->>StateManager: modify_state_with_links(client2_token, previous_dirty_vars)
    StateManager->>SharedState2: _modify_linked_states(previous_dirty_vars)
    SharedState2->>SharedState2: Apply dirty_vars to linked state
    StateManager->>Client2: Send delta update
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

6 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

reflex/app.py Outdated
self,
token: str,
background: bool = False,
previous_dirty_vars: set[str] | None = None,
Copy link
Contributor

Choose a reason for hiding this comment

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

syntax: Type mismatch: previous_dirty_vars is declared as set[str] | None here, but _modify_linked_states expects dict[str, set[str]] | None (line 144 in reflex/istate/shared.py). This will cause a runtime error when propagating changes to linked states.

Suggested change
previous_dirty_vars: set[str] | None = None,
previous_dirty_vars: dict[str, set[str]] | None = None,
Prompt To Fix With AI
This is a comment left during a code review.
Path: reflex/app.py
Line: 1568:1568

Comment:
**syntax:** Type mismatch: `previous_dirty_vars` is declared as `set[str] | None` here, but `_modify_linked_states` expects `dict[str, set[str]] | None` (line 144 in reflex/istate/shared.py). This will cause a runtime error when propagating changes to linked states.

```suggestion
        previous_dirty_vars: dict[str, set[str]] | None = None,
```

How can I resolve this? If you propose a fix, please make it concise.

async def modify_state_with_links(
self,
token: str,
previous_dirty_vars: set[str] | None = None,
Copy link
Contributor

Choose a reason for hiding this comment

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

syntax: Type mismatch: previous_dirty_vars is declared as set[str] | None but should be dict[str, set[str]] | None to match _modify_linked_states expectations.

Suggested change
previous_dirty_vars: set[str] | None = None,
previous_dirty_vars: dict[str, set[str]] | None = None,
Prompt To Fix With AI
This is a comment left during a code review.
Path: reflex/istate/manager/__init__.py
Line: 121:121

Comment:
**syntax:** Type mismatch: `previous_dirty_vars` is declared as `set[str] | None` but should be `dict[str, set[str]] | None` to match `_modify_linked_states` expectations.

```suggestion
        previous_dirty_vars: dict[str, set[str]] | None = None,
```

How can I resolve this? If you propose a fix, please make it concise.

masenf added 13 commits December 7, 2025 22:42
perform the subsequent updates in an asyncio.Task to allow the original caller
to drop the lock for the other shared states.
the state might have multiple links, and we may have already entered the
context and are holding the lock already, so we don't want to take the lock
again which will hang.

instead, check if the state is already linked to the target token and avoid
doing extra work.
_modify_linked_states context can now release the locks of newly linked states
and send updates for changes in newly linked states.

rehydrating after linking is no longer necessary.
definitely the token will be different. although private-dependent data should
use private states, it's common for llm generated code to define router_data
dependent vars in the linked state itself, so we make that special case work
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.

2 participants