Reflex Python
Intro
Reflex lets you build full-stack web apps in pure Python: UI is
composed from rx.* components (Python wrappers over React), state
lives in rx.State subclasses, and event handlers are ordinary
methods that mutate state. The framework compiles the frontend and
runs a Python backend that owns all business logic.
Overview
App structure
reflex init scaffolds rxconfig.py and the app package. The entry
point is app/app.py with app = rx.App(). Each page is a function
decorated with @rx.page(route="/path", title="..."). Files dropped
in app/pages/ become routes automatically. Configuration (app name,
database URL) lives in rxconfig.py. Static assets in assets/ are
served at /filename.
Components
Every piece of UI is an rx.* call. Layout helpers (rx.box,
rx.flex, rx.grid, rx.stack, rx.vstack, rx.hstack,
rx.center) nest display and input components. Props are keyword
arguments; children are positional. Reflex ships Radix-based
primitives for inputs, feedback, navigation, and display.
State management
Define state as a class inheriting from rx.State. State vars are
typed class attributes (count: int = 0) — the type annotation is
mandatory because Reflex uses it for serialization. Event handlers
are methods that modify self. Computed vars use @rx.var. Each
browser session gets its own state instance, and substates
(class FormState(rx.State)) keep large apps modular.
Event handlers and binding
Attach handlers to component events: rx.button("Add",>. Reflex auto-generates set_<var> for
every state var, enabling two-way binding like rx.input( value=State.query,>. Handlers can yield
between mutations to push intermediate UI updates. Long-running work
goes in @rx.event(background=True) handlers that use async with self: to mutate state safely.
Styling
Props drive styling: rx.box(padding="4", bg="blue.500", border_radius="md"). Values come from Radix design tokens (color
scales blue.1–blue.12, spacing, radii). Responsive props take a
list that maps to [mobile, tablet, desktop]. Global styles live in
rx.App(style={...}). Dark mode uses rx.color_mode.toggle and
rx.color_mode_cond().
Routing
@rx.page(route="/users/[user_id]") declares a dynamic route;
parameters are accessed via self.router.page.params["user_id"].
rx.link("Home", href="/") produces navigation links and
rx.redirect("/dashboard") returned from a handler performs a
programmatic navigation. Attach on_load=State.load_data to the page
decorator for per-visit initialization.
Database
Reflex ships SQLModel integration: class User(rx.Model, table=True): .... Query inside handlers via with rx.session() as session: ....
reflex db migrate handles schema migrations. Default SQLite for
development; configure PostgreSQL in rxconfig.py for production.
Gotchas
Agent-specific failure modes — provider-neutral pause-and-self-check items:
- Mutating state outside an event handler. Reflex tracks state mutations only inside event handler methods. Changing a state var directly from a background task or external callback without
async with self:silently drops the mutation — the UI never updates. All state mutations must go through event handlers or theasync with self:context manager in background tasks. - Missing type annotations on state vars. Reflex uses type annotations for serialization and code generation. A state var without a type annotation (
count = 0instead ofcount: int = 0) causes runtime serialization errors or is silently ignored. Every state var requires a type annotation. - Non-serializable values in state. Reflex must serialize state to JSON to sync it between backend and frontend. Storing a SQLAlchemy model instance, a file handle, or a datetime object without a JSON-compatible type in a state var causes silent failures or serialization errors. State vars must be JSON-serializable; derive and cache computed values via
@rx.var. - Blocking the event loop with CPU or I/O work inside a regular event handler. A slow synchronous event handler (network call, database query, heavy computation) blocks all other events for that session. Move blocking work into
@rx.event(background=True)async handlers. - One giant global state class for the entire app. All components subscribed to the same state class rebuild on any change. A single large state means unrelated UI panels rebuild together. Split state by feature domain into substates (
class CartState(rx.State),class UserState(rx.State)) to isolate rebuilds. - Not calling
yieldin a long-running synchronous handler. A handler that runs for several seconds withoutyieldblocks UI updates for the entire duration — the user sees a frozen screen.yieldbetween steps to push intermediate state updates to the frontend. - Hardcoding route paths as strings in multiple places. Route paths like
"/dashboard"scattered acrossrx.redirect(...)calls and@rx.page(route=...)decorators become maintenance debt when routes change. Define route constants once and reference them everywhere.
Full reference
Layout components
# Flex container (horizontal by default)
rx.flex(
rx.box("Item 1"),
rx.box("Item 2"),
direction="row", # "row" | "column"
gap="4",
align="center", # cross-axis alignment
justify="between", # main-axis distribution
wrap="wrap",
)
# Grid layout
rx.grid(
rx.box("A"), rx.box("B"), rx.box("C"), rx.box("D"),
columns="2",
spacing="4",
)
# Stack shorthands
rx.vstack(rx.text("Top"), rx.text("Bottom"), spacing="3")
rx.hstack(rx.text("Left"), rx.text("Right"), spacing="3")
# Center content
rx.center(rx.spinner(), height="100vh")
Display and input
rx.text("Body text", size="3", color="gray.11")
rx.heading("Page Title", size="7", weight="bold")
rx.image(src="/logo.png", alt="Logo", width="200px")
rx.badge("NEW", color_scheme="green", variant="soft")
rx.code_block(code, language="python", show_line_numbers=True)
rx.card(
rx.text("Card content"),
rx.button("Action"),
size="3",
)
rx.input(
placeholder="Search...",
value=State.query,
size="3",
)
rx.text_area(value=State.content, rows="5")
rx.select(
["Option A", "Option B", "Option C"],
value=State.selected,
placeholder="Choose one",
)
rx.checkbox("Accept terms", checked=State.accepted,
rx.switch(checked=State.dark_mode,
rx.button("Primary", color_scheme="blue")
rx.button("Outline", variant="outline")
rx.button("Loading", loading=State.is_loading,
State patterns
import reflex as rx
class AppState(rx.State):
# Simple vars (set_<name> auto-generated)
count: int = 0
items: list[str] = []
query: str = ""
# Event handlers
def increment(self):
self.count += 1
def add_item(self):
if self.query:
self.items.append(self.query)
self.query = ""
def remove_item(self, item: str):
self.items = [i for i in self.items if i != item]
# Computed var (read-only, auto-updates)
@rx.var
def item_count(self) -> int:
return len(self.items)
@rx.var
def filtered_items(self) -> list[str]:
if not self.query:
return self.items
return [i for i in self.items if self.query.lower() in i.lower()]
Event handler patterns
class FormState(rx.State):
form_data: dict = {}
# Handler with event data
def handle_submit(self, data: dict):
self.form_data = data
# Yielding for progress updates
def process(self):
self.status = "Starting..."
yield
self.status = "Processing..."
yield
self.status = "Done"
# Background task (non-blocking)
@rx.event(background=True)
async def fetch_data(self):
import httpx
async with httpx.AsyncClient() as client:
resp = await client.get("https://api.example.com/data")
async with self:
self.data = resp.json()
# Redirect after action
def login(self):
if self.authenticate():
return rx.redirect("/dashboard")
Conditional and dynamic rendering
# Conditional rendering
rx.cond(
State.logged_in,
rx.text("Welcome back!"),
rx.button("Log in",
)
# Rendering lists
rx.foreach(
State.items,
lambda item: rx.hstack(
rx.text(item),
rx.icon_button("x",
),
)
# Multi-branch
rx.match(
State.status,
("loading", rx.spinner()),
("error", rx.text("Error!", color="red")),
("success", rx.text("Done!", color="green")),
rx.text("Unknown"), # default
)
Pages and routing
@rx.page(route="/", title="Home",
def index() -> rx.Component:
return rx.box(
rx.heading("Home"),
rx.text("Welcome"),
)
@rx.page(route="/users/[user_id]", title="User Profile")
def user_profile() -> rx.Component:
return rx.box(
rx.text(f"User: {AppState.router.page.params['user_id']}"),
)
rx.link("Go Home", href="/")
rx.link(rx.button("Dashboard"), href="/dashboard")
Form handling
rx.form(
rx.vstack(
rx.input(name="email", placeholder="Email", required=True),
rx.input(name="password", type="password", required=True),
rx.button("Submit", type="submit"),
spacing="3",
),
reset_on_submit=True,
)
Deployment
- Reflex Cloud:
reflex deployfor managed hosting - Self-host:
reflex export --frontend-onlyproduces a static frontend plus a backend you run withuvicorn - Docker: export, then serve the frontend via nginx and the backend via uvicorn
- Set
REFLEX_ENV=prodfor production optimizations
Common mistakes
- Mutating state outside event handlers — all state mutation must happen inside a handler method
- Missing type annotations on state vars — Reflex needs them for serialization and code generation
- Non-serializable values in state — every state var must be JSON-serializable
- Blocking the event loop — push CPU/IO-heavy work into
@rx.event(background=True)handlers - One giant global state — split into substates by feature