Building Reactive UIs in Pure Python with Reflex's State Management
Learn how Reflex’s rx.Var and @rx.event enable automatic UI updates from pure Python, with a counter example, WebSocket verification, and notes on memory and mutation pitfalls.
13 Jan 2026, 03:50 UTC

The Problem: Keeping UI in Sync Without Writing JavaScript
When you want a web page that updates instantly as data changes, the usual path involves writing JavaScript event listeners, managing DOM updates, and often pulling in a frontend framework. For teams that prefer to stay in Python, this context switch adds overhead and can lead to mismatched state between the server and the browser.
How Reflex Handles Reactive State
Reflex introduces a rx.Var object that holds a piece of application state. Any time a Var is modified, the framework automatically computes the difference and pushes only that delta to connected clients over a WebSocket. On the client, the minimal JavaScript bundle generated by Reflex applies the diff, causing the UI to refresh without a full page reload.
State lives in a subclass of rx.State. Methods decorated with @rx.event run on the server, can update Var attributes, and trigger the reactive cycle. UI logic such as conditionals, loops, and derived values is expressed directly in the component tree using rx.cond, rx.foreach, and rx.match, keeping everything in Python.
Worked Example: A Simple Counter
- Create the project – run
reflex init mycounterin a terminal where you have write access to your user directory. This scaffolds a basic Reflex app. - Define state – edit
mycounter/mycounter.pyand add aStateclass:
import reflex as rx
class CounterState(rx.State):
count: rx.Var[int] = 0
@rx.event
def increment(self):
self.count = self.count + 1
@rx.event
def decrement(self):
self.count = self.count - 1
- Build the UI – still in the same file, add the component that binds to the state:
def index():
return rx.vstack(
rx.heading("Counter Demo"),
rx.text("Count: ", CounterState.count),
rx.button("Increment", on_click=CounterState.increment),
rx.button("Decrement", on_click=CounterState.decrement),
spacing="4",
align="center",
)
app = rx.App()
app.add_page(index)
app.compile()
- Run the app – execute
reflex runfrom the project root. The command starts a development server (default port 3000) and opens a browser window. - Verify the reactive update – click the Increment or Decrement button. The number displayed next to "Count:" changes instantly without a page reload.
- Inspect the WebSocket traffic – open the browser’s developer tools, go to the Network tab, filter for WS, and select the WebSocket connection. Each button press will show a binary frame; the payload contains only the updated
countvalue, illustrating the minimal diff transfer.
Trade‑offs and Limitations
- Memory usage per client – every connected browser keeps a full copy of the server‑side
Statein memory. Applications with many concurrent users may see higher RAM consumption on the client side. - Mutable object pitfalls – if you modify a mutable Python object (e.g., a list or dict) in place without using
Var.setor reassigning theVar, Reflex’s change detection will not notice the mutation. The UI will stay stale until another explicit update occurs. The safe pattern is to treatVarvalues as immutable and replace them:
# ❌ Wrong: mutating in place
self.my_list.append(item)
# ✅ Correct: create a new list and assign
self.my_list = self.my_list + [item]
Practical check: after adding an item via the incorrect pattern, observe that the UI does not reflect the change until you trigger another event (e.g., press a dummy button). Switching to the reassignment pattern restores immediate updates.
Actionable Closing
Reflex lets you build real‑time, interactive web applications while staying in Python, eliminating the need to write custom JavaScript for state synchronization. Start with a small component like the counter above, verify the WebSocket diff in the browser tools, and then scale to more complex state structures. Keep an eye on per‑client memory usage and always replace mutable objects rather than mutating them in place to ensure the UI stays in sync.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.