Choosing Between Shared State and Component-Scoped State in Reflex
Reflex state must live on rx.State or rx.ComponentState subclasses — plain Python variables are not reactive. Learn when to use shared state versus component-scoped state, and how to verify the behavior.
08 Jan 2026, 09:23 UTC

In Reflex, state changes made in Python event handlers are synchronized to the browser over a WebSocket connection. The framework computes which variables changed and sends those deltas to the frontend, so the practical question is not "how big is my state object" but "how is my state scoped, and how much of the UI depends on it." Choosing the wrong scope leads either to unnecessary coupling (everything lives in one giant state class) or to awkward plumbing (callbacks threaded through many components to share what should have been shared state).
The decision: put data in a shared rx.State subclass when multiple unrelated parts of the app read or write it, and use component-scoped state (via rx.ComponentState, available in recent Reflex versions) when the data belongs to one reusable component instance. Note that rx.ComponentState is a relatively recent addition; check the Reflex changelog for the exact version that introduced it in your pinned release before relying on it.
Comparing the Options
| Aspect | Shared State (rx.State subclass) | Component State (rx.ComponentState) |
|---|---|---|
| Scope | Accessible from any component via the state class. | Bound to one component instance; each rendered instance gets its own copy. |
| Sharing | Direct: reference MyState.field anywhere. | Other components cannot read it directly; you must lift the data up or pass callbacks. |
| Re-render reach | Any component referencing a changed var re-renders. | Only the owning component subtree re-renders. |
| Typical use | Authenticated user, app-wide settings, shared filters. | Reusable widgets: tabs, accordions, per-card edit forms, per-instance counters. |
| Hierarchy | Can be split into substates that inherit from a parent state. | Self-contained per component definition. |
How Reflex Actually Synchronizes State
Two common misconceptions are worth correcting up front. First, Reflex does not resend your entire state object on every change; it tracks dirty variables during an event handler and sends a delta containing the updated values. Second, plain Python variables (including closures with nonlocal) inside a component function are not reactive state. They are evaluated once when the page is built and will not update the UI. All mutable, UI-visible state must live on an rx.State (or rx.ComponentState) subclass as a class-level var, or be derived from such vars with @rx.var computed properties.
Because updates are deltas, the cost of a mutation is driven by the size of the changed values and by how many components reference them — not by the total size of the state class. A large list stored in state is only expensive when the list itself changes or when many components depend on it.
Shared State: A Working Example
Define shared state by subclassing rx.State. Event handlers are methods on the class; components reference vars through the class:
import reflex as rx
class AppState(rx.State):
user_name: str = ""
theme: str = "light"
@rx.event
def login(self, name: str):
self.user_name = name
@rx.event
def toggle_theme(self):
self.theme = "dark" if self.theme == "light" else "light"
def header() -> rx.Component:
return rx.hstack(
rx.text(f"Signed in as: {AppState.user_name}"),
rx.button(
f"Theme: {AppState.theme}",
on_click=AppState.toggle_theme,
),
)
If a single state class grows unwieldy, split it into substates that inherit from a common parent. Substates keep related vars and handlers together while remaining accessible app-wide, and a change in one substate does not dirty vars in a sibling substate.
Component-Scoped State: A Working Example
For a reusable widget whose state should be independent per instance, subclass rx.ComponentState and implement get_component:
import reflex as rx
class CounterState(rx.ComponentState):
count: int = 0
@rx.event
def increment(self):
self.count += 1
@classmethod
def get_component(cls, **props) -> rx.Component:
return rx.hstack(
rx.text(f"Count: {cls.count}"),
rx.button("+1", on_click=cls.increment),
**props,
)
def index() -> rx.Component:
# Each call creates an independent counter instance.
return rx.vstack(
CounterState.create(),
CounterState.create(),
)
app = rx.App()
app.add_page(index)
Clicking one counter's button updates only that instance. This is the correct replacement for the (broken) pattern of using a Python closure variable inside a component function. The trade-off: nothing outside the component can see count. If a parent page later needs the total of all counters, component state is the wrong scope — lift the data into a shared state class instead.
Validating the Behavior
You can verify scoping empirically without special tooling:
- Run the app (
reflex runfrom the project directory) and open it in two different browsers. Increment a shared-state value in one browser: the other browser should not change, because Reflex state is per-session. This confirms session isolation, not sharing across users — shared state is shared across components, not across clients. - Within one browser, render two instances of a
ComponentStatewidget and confirm they update independently. - Open Developer Tools → Network → WS and inspect the WebSocket frames after clicking a button. You should see a small delta payload containing the changed var(s), not a dump of the full state. If a frame contains vars you did not touch, check for computed vars (
@rx.var) that depend on the changed value — they are recomputed and included when their dependencies change.
Limitations
Reflex state lives in server-side memory tied to the client session. It does not survive a server restart or session expiry, and it is not shared between users. If data must persist or be visible across sessions, write it to a database inside an event handler and load it on page load (for example via an on_load event). Also note that the exact API surface — particularly rx.ComponentState and the @rx.event decorator — varies across Reflex versions; verify against the documentation for your installed version (pip show reflex) before adopting these patterns.
Takeaway
Default to shared rx.State subclasses for data read by more than one part of the app, and split large states into substates as they grow. Reach for rx.ComponentState when building reusable widgets whose state is private to each instance. Never rely on plain Python variables for reactive UI state, and confirm your assumptions by watching the WebSocket frames in the browser's Network tab.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.