Reflex Reactive State: Interactive Python UIs Without Callback Hell
Reflex lets you build interactive web apps by treating UI state as plain Python attributes. Here is how its reactive variable system works, where it shines, and when you need an external store.
26 Nov 2025, 21:32 UTC

Adding interactivity to a Python web app usually forces a choice: accept full page reloads for every click, or context-switch into JavaScript to manage DOM state. Reflex offers a third path. Its reactive state system lets you declare UI state as ordinary Python attributes. When you change an attribute inside an event handler, the framework pushes the update to the browser automatically.
The practical takeaway: you can build a counter, a form, or a dashboard using Python classes and methods, without writing callback glue code or manually diffing the DOM.
Reactive variables are just Python attributes
In Reflex, application state lives in a class that inherits from rx.State. Fields on that class behave like reactive variables—conceptually similar to rx.Var—meaning the framework tracks assignments to them.
import reflex as rx
class State(rx.State):
count: int = 0
def increment(self):
self.count += 1
When increment runs, Reflex does not require you to call a separate “update UI” function. The assignment to self.count is enough to schedule a frontend update. Under the hood, Reflex batches these changes through an internal queue so that multiple rapid mutations do not flood the websocket connection.
Declarative binding keeps components in sync
Components read state by referencing the class attribute directly in their props. Event handlers are passed as references to state methods.
rx.button(
"Increment",
on_click=State.increment,
)
The same pattern applies to inputs. Reflex generates setter methods for state fields automatically, so you can bind the value and the handler in one line without writing a separate change callback:
rx.input(
value=State.text,
on_change=State.set_text,
)
This declarative style removes the manual synchronization step. The component tree always reflects the current state object.
Computed vars derive values automatically
Reflex supports computed vars: methods decorated to act as derived values. They recompute when their dependencies change, much like a spreadsheet formula.
@rx.var
def double_count(self) -> int:
return self.count * 2
You reference State.double_count in a component exactly like a base field. If count changes, double_count updates without extra code. This keeps transformation logic in Python rather than duplicating it in the frontend.
A minimal counter with derived state
Here is a complete, minimal app. Save it as a Reflex page (for example, counter/counter.py) inside an initialized Reflex project.
import reflex as rx
class CounterState(rx.State):
count: int = 0
def increment(self):
self.count += 1
@rx.var
def double_count(self) -> int:
return self.count * 2
def index():
return rx.vstack(
rx.button("Increment", on_click=CounterState.increment),
rx.text("Count: ", CounterState.count),
rx.text("Double: ", CounterState.double_count),
)
app = rx.App(state=CounterState)
app.add_page(index, title="Counter")
Where to run it: from the project root in your terminal, run reflex run. No elevated permissions are required for local development.
Expected check: open the local URL Reflex prints. Clicking the button should increment both the Count and Double text elements instantly. If the numbers do not change, confirm that the event handler reference (CounterState.increment) is passed without parentheses and that the page is registered with app.add_page.
Risk: because state is held in memory by default, restarting the server resets count to 0. This is fine for a prototype but matters for production. Because Reflex’s API evolves, verify the exact decorator and import names against the version installed in your project.
The in-memory limitation and scaling trade-off
Reflex state is stored in memory on the server. For a single-worker local app this is fast and simple. Two constraints appear as you scale.
First, memory usage. If a reactive var holds a large dataset—say, a list of thousands of records—that memory stays resident as long as the state object exists. Long-running instances can accumulate usage if state is not cleared between user sessions.
Second, concurrency across workers. The reactive system assumes single-threaded execution within each server worker. If you run multiple Reflex instances behind a load balancer, a state change in one worker is invisible to the others. Coordinating shared state requires an external store such as Redis or a database, with your own synchronization logic.
Practical verification: to check for leaks in a simple app, monitor the server process with psutil while interacting rapidly. Record resident memory before and after a sustained interaction period. If memory climbs steadily and does not plateau, investigate whether large objects are being retained in reactive vars. For multi-instance behavior, run two workers pointed at the same external store and confirm that a mutation in one appears in the other; if it does not, you have confirmed the boundary of Reflex’s built-in state model.
Choosing the right state boundary
Use Reflex reactive state for UI-local concerns: form values, toggle flags, pagination indexes, and derived display strings. Reach for an external store when state must survive restarts, be shared across workers, or exceed comfortable memory limits. The reactive system removes frontend boilerplate, but it does not replace your application’s persistence layer.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.