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))
        else:
            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 improve performances by 3.38%

Comparing masenf/linked-state (b9fe004) with main (7826d0b)

Summary

⚡ 2 improvements
✅ 6 untouched

Benchmarks breakdown

Benchmark BASE HEAD Change
test_get_all_imports[_stateful_page] 3.2 ms 3.1 ms +3.38%
test_get_all_imports[_complicated_page] 23.8 ms 23.1 ms +3.12%

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Dec 6, 2025

Greptile Overview

Greptile Summary

Implements an API for linking and sharing states across multiple clients by introducing SharedState class and supporting infrastructure. The feature allows clients to link to a shared token and have state changes propagate to all other clients linked to that token.

Key Changes:

  • New SharedState base class with _link_to() and _unlink() methods for managing shared state linkage
  • StateManager.modify_state_with_links() method that patches linked states into the current state tree
  • Background task system to propagate changes to other linked clients without blocking the event handler
  • Comprehensive integration tests covering linking, unlinking, re-linking, and concurrent modifications

Main Issue Found:

  • Logic bug in _link_to() at reflex/istate/shared.py:182 where _linked_from cleanup operates on the wrong state instance when re-linking

Confidence Score: 4/5

  • This PR is mostly safe to merge with one critical logic bug that needs fixing
  • Score reflects excellent test coverage and clean architecture, but reduced by 1 due to the _linked_from cleanup bug that could cause unnecessary state propagation when clients switch between shared tokens
  • reflex/istate/shared.py needs attention for the linkage cleanup bug at line 180-182

Important Files Changed

File Analysis

Filename Score Overview
reflex/istate/shared.py 4/5 New shared state implementation with link/unlink functionality; includes potential issue with _linked_from cleanup when re-linking
reflex/istate/manager/init.py 5/5 Added modify_state_with_links method to handle linked states; clean implementation with proper context management
reflex/app.py 5/5 Updated to use modify_state_with_links in place of modify_state at key entry points; minimal changes, well-integrated
reflex/state.py 5/5 Added _reflex_internal_links backend var and _override_base_method decorator; clean additions to support linked state feature
tests/integration/test_linked_state.py 5/5 Comprehensive integration tests covering linking, unlinking, re-linking, background tasks, and multi-tab scenarios

Sequence Diagram

sequenceDiagram
    participant C1 as Client1
    participant C2 as Client2
    participant A as App
    participant SM as StateManager
    participant SS as SharedState

    Note over C1,SS: Initial Linking
    C1->>A: link_to(shared_token)
    A->>SM: modify_state_with_links
    SM->>A: client1_state
    A->>SS: perform link
    SS->>SM: get shared_state (lock)
    SS->>SS: add client1 to linked_from
    SS->>SS: patch into tree
    SS-->>C1: delta

    Note over C1,SS: Second Client Links
    C2->>A: link_to(shared_token)
    A->>SM: modify_state_with_links
    SM->>A: client2_state
    A->>SS: perform link
    SS->>SM: get shared_state (lock)
    SS->>SS: add client2 to linked_from
    SS->>SS: patch into tree
    SS-->>C2: delta

    Note over C1,SS: Modify Shared State
    C1->>A: modify shared state
    A->>SM: modify_state_with_links
    A->>SS: apply changes
    SS->>SS: track dirty vars
    SS->>A: exit context
    A->>SM: update_other_tokens(C2)
    SM-->>C2: delta (async)

    Note over C1,SS: Unlink
    C1->>A: unlink
    A->>SM: modify_state_with_links
    A->>SS: perform unlink
    SS->>SS: remove from links
    SS->>SM: get private state
    SS->>SS: patch private back
    SS-->>C1: rehydrate
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

masenf added 14 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
@masenf
Copy link
Collaborator Author

masenf commented Dec 12, 2025

@greptileai

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.

9 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements a new API for linking and sharing state between multiple clients in Reflex applications. It introduces the SharedState base class that allows different clients to share state by linking to a common token, enabling real-time state synchronization across sessions.

Key Changes:

  • Introduces rx.SharedState mixin class with _link_to() and _unlink() methods for state sharing
  • Adds _reflex_internal_links backend variable to track linked state mappings
  • Implements automatic state synchronization when linked states are modified

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
reflex/istate/shared.py New file implementing SharedState base classes and linking mechanisms
reflex/istate/manager/__init__.py Adds modify_state_with_links() to handle linked state modifications
reflex/app.py Updates state modification calls to use modify_state_with_links()
reflex/state.py Adds _override_base_method decorator and _reflex_internal_links backend variable
reflex/__init__.py Exports SharedState for public API
tests/integration/test_linked_state.py Comprehensive integration tests for linked state functionality
tests/units/test_state.py Updates test assertions to include new backend variable
tests/integration/test_computed_vars.py Updates test assertions to include new backend variable
pyi_hashes.json Updates type stub hash for API changes

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@adhami3310 adhami3310 merged commit 2907722 into main Dec 15, 2025
47 checks passed
@adhami3310 adhami3310 deleted the masenf/linked-state branch December 15, 2025 20:45
@larsblumberg
Copy link

Hi @masenf this sounds like a great piece of a new functionality. Which use cases inspired you to implement this? Asked differently, when exactly should we consider using shared/linked states?

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.

4 participants