Skip to content

Commit 5a3cd26

Browse files
Docs for linked SharedState (#1712)
* Docs for linked SharedState * Apply suggestions from code review Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Copilot suggestions --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
1 parent b19f84c commit 5a3cd26

File tree

3 files changed

+1178
-780
lines changed

3 files changed

+1178
-780
lines changed
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
```python exec
2+
import reflex as rx
3+
from pcweb.templates.docpage import definition
4+
```
5+
6+
# Shared State
7+
8+
_New in version 0.8.23_.
9+
10+
Defining a subclass of `rx.SharedState` creates a special type of state that may be shared by multiple clients. Shared State is useful for creating real-time collaborative applications where multiple users need to see and interact with the same data simultaneously.
11+
12+
## Using SharedState
13+
14+
An `rx.SharedState` subclass behaves similarly to a normal `rx.State` subclass and will be private to each client until it is explicitly linked to a given token. Once linked, any changes made to the Shared State by one client will be propagated to all other clients sharing the same token.
15+
16+
```md alert info
17+
# What should be used as a token?
18+
19+
A token can be any string that uniquely identifies a group of clients that should share the same state. Common choices include room IDs, document IDs, or user group IDs. Ensure that the token is securely generated and managed to prevent unauthorized access to shared state.
20+
```
21+
22+
```md alert warning
23+
# Linked token cannot contain underscore (_) characters.
24+
25+
Underscore characters are currently used as an internal delimiter for tokens and will raise an exception if used for linked states.
26+
27+
This is a temporary restriction and will be removed in a future release.
28+
```
29+
30+
### Linking Shared State
31+
32+
An `rx.SharedState` subclass can be linked to a token using the `_link_to` method, which is async and returns the linked state instance. After linking, subsequent events triggered against the shared state will be executed in the context of the linked state. To unlink from the token, return the result of awaiting the `_unlink` method.
33+
34+
To try out the collaborative counter example, open this page in a second or third browser tab and click the "Link" button. You should see the count increment in all tabs when you click the "Increment" button in any of them.
35+
36+
```python demo exec
37+
class CollaborativeCounter(rx.SharedState):
38+
count: int = 0
39+
40+
@rx.event
41+
async def toggle_link(self):
42+
if self._linked_to:
43+
return await self._unlink()
44+
else:
45+
linked_state = await self._link_to("shared-global-counter")
46+
linked_state.count += 1 # Increment count on link
47+
48+
@rx.var
49+
def is_linked(self) -> bool:
50+
return bool(self._linked_to)
51+
52+
def shared_state_example():
53+
return rx.vstack(
54+
rx.text(f"Collaborative Count: {CollaborativeCounter.count}"),
55+
rx.cond(
56+
CollaborativeCounter.is_linked,
57+
rx.button("Unlink", on_click=CollaborativeCounter.toggle_link),
58+
rx.button("Link", on_click=CollaborativeCounter.toggle_link),
59+
),
60+
rx.button("Increment", on_click=CollaborativeCounter.set_count(CollaborativeCounter.count + 1)),
61+
)
62+
```
63+
64+
```md alert info
65+
# Computed vars may reference SharedState
66+
67+
Computed vars in other states may reference shared state data using `get_state`, just like private states. This allows private states to provide personalized views of shared data.
68+
69+
Whenever the shared state is updated, any computed vars depending on it will be re-evaluated in the context of each client's private state.
70+
```
71+
72+
### Identifying Clients
73+
74+
Each client linked to a shared state can be uniquely identified by their `self.router.session.client_token`. Shared state events should _never_ rely on identifiers passed in as parameters, as these can be spoofed from the client. Instead, always use the `client_token` to identify the client triggering the event.
75+
76+
```python demo exec
77+
import uuid
78+
79+
class SharedRoom(rx.SharedState):
80+
shared_room: str = rx.LocalStorage()
81+
_users: dict[str, str] = {}
82+
83+
@rx.var
84+
def user_list(self) -> str:
85+
return ", ".join(self._users.values())
86+
87+
@rx.event
88+
async def join(self, username: str):
89+
if not self.shared_room:
90+
self.shared_room = f"shared-room-{uuid.uuid4()}"
91+
linked_state = await self._link_to(self.shared_room)
92+
linked_state._users[self.router.session.client_token] = username
93+
94+
@rx.event
95+
async def leave(self):
96+
if self._linked_to:
97+
return await self._unlink()
98+
99+
100+
class PrivateState(rx.State):
101+
@rx.event
102+
def handle_submit(self, form_data: dict):
103+
return SharedRoom.join(form_data["username"])
104+
105+
@rx.var
106+
async def user_in_room(self) -> bool:
107+
shared_state = await self.get_state(SharedRoom)
108+
return self.router.session.client_token in shared_state._users
109+
110+
111+
def shared_room_example():
112+
return rx.vstack(
113+
rx.text("Shared Room"),
114+
rx.text(f"Users: {SharedRoom.user_list}"),
115+
rx.cond(
116+
PrivateState.user_in_room,
117+
rx.button("Leave Room", on_click=SharedRoom.leave),
118+
rx.form(
119+
rx.input(placeholder="Enter your name", name="username"),
120+
rx.button("Join Room"),
121+
on_submit=PrivateState.handle_submit,
122+
),
123+
),
124+
)
125+
```
126+
127+
```md alert warning
128+
# Store sensitive data in backend-only vars with an underscore prefix
129+
130+
Shared State data is synchronized to all linked clients, so avoid storing sensitive information (e.g., client_tokens, user credentials, personal data) in frontend vars, which would expose them to all users and allow them to be modified outside of explicit event handlers. Instead, use backend-only vars (prefixed with an underscore) to keep sensitive data secure on the server side and provide controlled access through event handlers and computed vars.
131+
```
132+
133+
### Introspecting Linked Clients
134+
135+
An `rx.SharedState` subclass has two attributes for determining link status and peers, which are updated during linking and unlinking, and come with some caveats.
136+
137+
**`_linked_to: str`**
138+
139+
Provides the token that the state is currently linked to, or empty string if not linked.
140+
141+
This attribute is only set on the linked state instance returned by `_link_to`. It will be an empty string on any unlinked shared state instances. However, if another state links to a client's private token, then the `_linked_to` attribute will be set to the client's token rather than an empty string.
142+
143+
When `_linked_to` equals `self.router.session.client_token`, it is assumed that the current client is unlinked, but another client has linked to this client's private state. Although this is possible, it is generally discouraged to link shared states to private client tokens.
144+
145+
**`_linked_from: set[str]`**
146+
147+
A set of client tokens that are currently linked to this shared state instance.
148+
149+
This attribute is only updated during `_link_to` and `_unlink` calls. In situations where unlinking occurs otherwise, such as client disconnects, `self.reset()` is called, or state expires on the backend, `_linked_from` may contain stale client tokens that are no longer linked. These can be cleaned periodically by checking if the tokens still exist in `app.event_namespace.token_to_sid`.
150+
151+
## Guidelines and Best Practices
152+
153+
### Keep Shared State Minimal
154+
155+
When defining a shared state, aim to keep it as minimal as possible. Only include the data and methods that need to be shared between clients. This helps reduce complexity and potential synchronization issues.
156+
157+
Linked states are always loaded into the tree for each event on each linked client and large states take longer to serialize and transmit over the network. Because linked states are regularly loaded in the context of many clients, they incur higher lock contention, so minimizing loading time also reduces lock waiting time for other clients.
158+
159+
### Prefer Backend-Only Vars in Shared State
160+
161+
A shared state should primarily use backend-only vars (prefixed with an underscore) to store shared data. Often, not all users of the shared state need visibility into all of the data in the shared state. Use computed vars to provide sanitized access to shared data as needed.
162+
163+
```python
164+
from typing import Literal
165+
166+
class SharedGameState(rx.SharedState):
167+
# Sensitive user metadata stored in backend-only variable.
168+
_players: dict[str, Literal["X", "O"]] = {}
169+
170+
@rx.event
171+
def make_move(self, x: int, y: int):
172+
# Identify users by client_token, never by arguments passed to the event.
173+
player_token = self.router.session.client_token
174+
player_piece = self._players.get(player_token)
175+
```
176+
177+
```md alert warning
178+
# Do Not Trust Event Handler Arguments
179+
180+
The client can send whatever data it wants to event handlers, so never rely on arguments passed to event handlers for sensitive information such as user identity or permissions. Always use secure identifiers like `self.router.session.client_token` to identify the client triggering the event.
181+
```
182+
183+
### Expose Per-User Data via Private States
184+
185+
If certain data in the shared state needs to be personalized for each user, prefer to expose that data through computed vars defined in private states. This allows each user to have their own view of the shared data without exposing sensitive information to other users. It also reduces the amount of unrelated data sent to each client and improves caching performance by keeping each user's view cached in their own private state, rather than always recomputing the shared state vars for each user that needs to have their information updated.
186+
187+
Use async computed vars with `get_state` to access shared state data from private states.
188+
189+
```python
190+
class UserGameState(rx.State):
191+
@rx.var
192+
async def player_piece(self) -> str | None:
193+
shared_state = await self.get_state(SharedGameState)
194+
return shared_state._players.get(self.router.session.client_token)
195+
```
196+
197+
### Use Dynamic Routes for Linked Tokens
198+
199+
It is often convenient to define dynamic routes that include the linked token as part of the URL path. This allows users to easily share links to specific shared state instances. The dynamic route can use `on_load` to link the shared state to the token extracted from the URL.
200+
201+
```python
202+
class SharedRoom(rx.SharedState):
203+
async def on_load(self):
204+
# `self.room_id` is the automatically defined dynamic route var.
205+
await self._link_to(self.room_id.replace("_", "-") or "default-room")
206+
207+
208+
def room_page(): ...
209+
210+
211+
app.add_route(
212+
room_page,
213+
path="/room/[room_id]",
214+
on_load=SharedRoom.on_load,
215+
)
216+
```

pcweb/components/docpage/sidebar/sidebar_items/learn.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ def get_sidebar_items_backend():
160160
state_structure.overview,
161161
state_structure.component_state,
162162
state_structure.mixins,
163+
state_structure.shared_state,
163164
],
164165
),
165166
create_item(

0 commit comments

Comments
 (0)