Building a reactive counter in Reflex without JavaScript
How Reflex moves state from Python event handlers to the browser, and why each tab starts with its own counter.
30 Sept 2026, 04:38 UTC

The problem: interactive Python UIs without a full reload
You want a button click or form change to update the screen, but you do not want to write a separate JavaScript frontend or manage an API by hand. Reflex lets you write the UI and its behavior in Python. The part that often causes confusion is not the syntax—it is what happens to data after an event fires.
The useful takeaway: Reflex stores reactive data in classes that inherit from rx.State. When a bound event handler runs on the server, Reflex sends only the changed state to the browser, and the affected components re-render without reloading the page. By default, that state belongs to one browser session, so two users—or two tabs—do not share a counter unless you configure shared state explicitly.
What rx.State and event handlers actually do
rx.State is the base class for reactive state. Public variables on a subclass become state values. Methods on that class become event handlers when you bind them to a component, such as on_click=CounterState.increment.
When the browser triggers the event:
- Reflex sends the event name to the server.
- The server runs the handler for the current session and mutates state variables.
- Reflex serializes the updated state and sends it to the frontend.
- The frontend re-renders only the components that read those variables.
This is not a full document reload. In browser developer tools, you should see incremental messages rather than a new HTML page load.
Worked example: a single-file counter
Create a file named counter_app.py:
import reflex as rx
class CounterState(rx.State):
count: int = 0
def increment(self):
self.count += 1
def decrement(self):
self.count -= 1
def index():
return rx.hstack(
rx.button("Decrement", on_click=CounterState.decrement),
rx.heading(CounterState.count),
rx.button("Increment", on_click=CounterState.increment),
)
app = rx.App()
app.add_page(index)
Run these commands in a terminal where you can install Python packages. Run reflex run from the directory containing counter_app.py.
python -m venv .venv
# Linux or macOS:
source .venv/bin/activate
# Windows PowerShell:
# .venv\Scripts\Activate.ps1
pip install reflex
reflex --version
reflex run
reflex --version confirms the installed release. Because Reflex is pre-1.0 in many distributions, module names and event signatures can change between versions, so check the version before copying examples from other sources.
Open the URL printed by reflex run, usually http://127.0.0.1:8000. Click Increment and the number should change without a page reload. Open browser developer tools, select the Network tab, and click again. You should see state-update traffic rather than a full page navigation.
Check that state is per-session
Open the same URL in a second browser or an incognito window. Click Increment in the first window. The second window should still show its own starting value. That isolation is the default session scope: each browser connection receives an independent copy of CounterState.
Limits and what to verify before scaling
- No automatic replication. Per-session state lives on the server instance that owns the connection. If you run multiple Reflex instances behind a load balancer, a user whose requests bounce between instances may lose state unless you use sticky sessions or an external store.
- Chatty inputs. Binding every keystroke to a server handler generates network traffic on each change. For high-frequency input, debounce the input or validate locally before sending events.
- Debugging surface. Handler errors appear in server logs, and the generated frontend code can be harder to inspect than plain JavaScript.
Practical verification checklist
- Run
reflex --versionand record it; note that APIs may differ in later releases. - Run the counter app and confirm the number updates without a reload.
- Open two browsers and confirm each counter is independent.
- Watch the Network tab during a click to confirm incremental state messages.
If you need state shared across users or tabs, look into Reflex's shared-state options rather than assuming rx.State variables are global. For a single-user dashboard or per-user form, the default session scope is usually the right starting point.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.