Reflex's Single‑Source‑of‑Truth State: Why It Works and Where It Drags
Reflex puts all mutable UI state in one Python class on the server, syncing via WebSocket. That eliminates dual‑state bugs but adds latency for high‑frequency interactions. A tiny counter demo shows the flow and how to verify the generated Next.js bundle.
11 Oct 2025, 07:44 UTC

The Problem: Dual State Synchronization
Traditional full‑stack Python apps keep UI state in two places: a React (or Vue) store on the client and a Python model on the server. Every interaction requires an API call, a serializer, and a manual merge back into the frontend store. Bugs appear when the two copies drift—missed updates, stale caches, or race conditions on optimistic UI.
Reflex's Answer: One Python State Class
Reflex (formerly Pynecone) eliminates that duplication by making a single State class the authoritative source. The class lives on the FastAPI backend; the compiled Next.js frontend receives computed values over a persistent WebSocket and sends events back. Because the frontend never holds mutable state, there is no synchronization logic to write or debug.
Worked Example: A Counter That Lives on the Server
Create a minimal project and watch the WebSocket traffic to see the model in action.
- Initialize (run in a terminal with Python 3.11+ and
reflexinstalled):reflex init mycounter cd mycounter - Edit
mycounter/mycounter.pyto define a counter state and a page: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() -> rx.Component: return rx.vstack( rx.heading(CounterState.count), rx.hstack( rx.button("-", on_click=CounterState.decrement), rx.button("+", on_click=CounterState.increment), ), ) app = rx.App() app.add_page(index) - Run the dev server (requires network access on ports 3000 and 8000):
reflex run - Inspect the WebSocket: open Chrome DevTools → Network → WS filter. You’ll see a single
ws://localhost:8000/_eventconnection. Each button click sends a JSON event like{"event": "increment", "state": "CounterState"}and the server replies with the updatedcountvalue.
Expected check: the heading updates instantly without a full page reload. Risk: the dev server recompiles the Next.js frontend on every Python change; with >200 components the hot‑reload latency becomes noticeable.
Trade‑off: WebSocket Latency on High‑Frequency Updates
Because every user action round‑trips to the backend, interactions that fire dozens of events per second (drag‑and‑drop, keystroke‑by‑keystroke filtering, real‑time drawing) incur measurable lag on connections above ~100 ms RTT. The research notes that the compiled Next.js bundle still ships the full React runtime (~130 kB gzipped), so even static pages pay that cost unless you enable rx.config.export_static=True for purely client‑side routes.
| Scenario | Reflex (WebSocket) | Traditional React + REST |
|---|---|---|
| Low‑frequency clicks, forms | Simple, no extra API code | Boilerplate endpoints, serializers |
| High‑frequency drag, typing | Potential perceptible delay | Local state stays instant |
| Offline / poor connectivity | Fails without server | Can cache locally |
When to Choose Reflex (and When Not To)
- Choose Reflex when you want a single Python codebase, rapid CRUD‑style UIs, and can tolerate a WebSocket round‑trip per interaction.
- Avoid Reflex for latency‑sensitive collaborative editors, games, or any feature that demands sub‑100 ms local feedback without a reliable low‑latency link.
- Verify before committing: run
reflex export --frontend-onlyand inspect.web/for bundle size; deploy the generated Dockerfile to a staging container (e.g.,fly launch --dockerfile Dockerfile) and hit both/health(FastAPI) and/(Next.js) to confirm the production artifact works.
Reflex’s single‑source‑of‑truth model removes an entire class of synchronization bugs, but the WebSocket backbone makes it a poor fit for high‑frequency, low‑latency UI. Treat the framework as a productivity accelerator for typical admin panels, dashboards, and forms—not as a drop‑in replacement for client‑heavy React applications.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.