reflex — Full-Stack Web Apps in Pure Python#
What it is#
Reflex is a Python framework for building full-stack web applications without writing JavaScript. You define UI components and application state in Python; Reflex compiles the frontend to React, runs a FastAPI backend, and synchronises state between them over WebSockets. Every interaction — button click, form submit, URL navigation — is handled by Python event handlers on the server. The result is a single-language codebase for apps that would otherwise require React + Python API.
Install#
pip install reflex
reflex init # creates a new project in the current directory
reflex run # starts dev server at http://localhost:3000
Output: (none — exits 0 on success)
Quick example#
import reflex as rx
class CounterState(rx.State):
count: int = 0
def increment(self):
self.count += 1
def decrement(self):
self.count -= 1
def counter_page() -> rx.Component:
return rx.center(
rx.vstack(
rx.heading(f"Count: {CounterState.count}", size="5"),
rx.hstack(
rx.button("−", on_click=CounterState.decrement),
rx.button("+", on_click=CounterState.increment),
),
),
)
app = rx.App()
app.add_page(counter_page, route="/")
When / why to use it#
- Internal tools and dashboards where shipping a React + Python split is too much overhead.
- Data apps (like Streamlit) but with full routing, forms, and auth — where Streamlit’s re-run model breaks down.
- Rapid prototyping of full-stack features when your team knows Python but not React/TypeScript.
- AI demos and chatbot UIs that need streaming and real-time state updates.
- Applications that would otherwise use Dash or Gradio but need more UI flexibility.
Common pitfalls#
[!WARNING] State mutations must happen inside event handlers — you cannot mutate
self.fieldoutside an event handler method. Direct assignment in__init__or other methods is silently ignored or raises an error.
[!WARNING]
rx.Statefields must be typed — untyped fields are not tracked by Reflex’s reactivity system. Always annotate:count: int = 0, notcount = 0.
[!WARNING]
reflex runruns both frontend and backend — the first run compiles the React frontend (~30s). Subsequent runs are faster. Do not kill the process during the first compile.
[!TIP] Use
rx.varfor computed/derived properties that depend on other state fields. They update automatically when their dependencies change, just like React’suseMemo.
[!TIP]
yieldinside an event handler streams intermediate state updates to the frontend. Use it to show progress during long-running operations.
State — the reactive core#
rx.State is the single source of truth. Fields declared on a state class are synchronised to the frontend automatically. Event handlers are methods that mutate fields.
import reflex as rx
from typing import Optional
class AppState(rx.State):
# Reactive fields — changes trigger frontend re-render
message: str = ""
items: list[str] = []
loading: bool = False
selected: Optional[str] = None
# Computed property — recalculated when items changes
@rx.var
def item_count(self) -> int:
return len(self.items)
@rx.var
def has_items(self) -> bool:
return len(self.items) > 0
# Event handlers — called by UI events
def add_item(self, item: str):
if item.strip():
self.items.append(item.strip())
self.message = f"Added: {item}"
def remove_item(self, item: str):
self.items = [i for i in self.items if i != item]
def clear_all(self):
self.items = []
self.message = "Cleared"
def select_item(self, item: str):
self.selected = item
Components — building UI#
Reflex wraps every HTML element and many higher-level components. All accept Python keyword arguments for props and event handlers.
import reflex as rx
def item_card(item: str) -> rx.Component:
return rx.box(
rx.hstack(
rx.text(item),
rx.button(
"×",
on_click=lambda: AppState.remove_item(item),
color_scheme="red",
size="1",
),
),
border="1px solid #ccc",
border_radius="8px",
padding="8px",
)
def item_list() -> rx.Component:
return rx.vstack(
rx.foreach(AppState.items, item_card),
width="100%",
)
rx.foreach — iterate over state lists#
rx.foreach renders a component for each item in a reactive list. Unlike a Python for loop, it re-renders only changed items when the list updates.
import reflex as rx
class ListState(rx.State):
fruits: list[str] = ["Apple", "Banana", "Cherry"]
def remove(self, fruit: str):
self.fruits = [f for f in self.fruits if f != fruit]
def fruit_item(fruit: str) -> rx.Component:
return rx.hstack(
rx.text(fruit),
rx.icon_button(
rx.icon("trash"),
on_click=ListState.remove(fruit),
variant="ghost",
size="1",
),
)
def fruit_list() -> rx.Component:
return rx.vstack(
rx.heading("Fruits"),
rx.foreach(ListState.fruits, fruit_item),
)
Conditional rendering — rx.cond#
rx.cond renders one of two components based on a reactive boolean expression. It is the Reflex equivalent of {condition ? A : B} in JSX.
import reflex as rx
class AuthState(rx.State):
logged_in: bool = False
username: str = ""
def login(self, username: str):
self.logged_in = True
self.username = username
def logout(self):
self.logged_in = False
self.username = ""
def nav_bar() -> rx.Component:
return rx.hstack(
rx.text("My App"),
rx.cond(
AuthState.logged_in,
rx.hstack(
rx.text(f"Hello, {AuthState.username}"),
rx.button("Log out", on_click=AuthState.logout),
),
rx.button("Log in", on_click=lambda: AuthState.login("Alice Dev")),
),
)
Forms and input binding#
import reflex as rx
class FormState(rx.State):
name: str = ""
email: str = ""
submitted: bool = False
result: dict = {}
def handle_submit(self, form_data: dict):
self.result = form_data
self.submitted = True
def contact_form() -> rx.Component:
return rx.form(
rx.vstack(
rx.input(placeholder="Your name", name="name"),
rx.input(placeholder="Your email", name="email", type="email"),
rx.text_area(placeholder="Message", name="message"),
rx.button("Submit", type="submit"),
),
on_submit=FormState.handle_submit,
reset_on_submit=True,
)
def contact_page() -> rx.Component:
return rx.cond(
FormState.submitted,
rx.callout(f"Received: {FormState.result}", icon="check"),
contact_form(),
)
Async event handlers and streaming#
Async event handlers can yield to stream intermediate state updates — ideal for showing progress or streaming LLM output.
import reflex as rx
import asyncio
class StreamState(rx.State):
words: list[str] = []
generating: bool = False
async def generate_words(self):
self.generating = True
self.words = []
yield # stream initial state to frontend
sentences = ["Hello", "World", "from", "Reflex", "streaming"]
for word in sentences:
await asyncio.sleep(0.4)
self.words.append(word)
yield # stream each word as it arrives
self.generating = False
yield
def stream_page() -> rx.Component:
return rx.vstack(
rx.button(
"Generate",
on_click=StreamState.generate_words,
loading=StreamState.generating,
),
rx.hstack(rx.foreach(StreamState.words, rx.text)),
)
Multiple pages and routing#
import reflex as rx
def home() -> rx.Component:
return rx.vstack(
rx.heading("Home"),
rx.link("Go to About", href="/about"),
)
def about() -> rx.Component:
return rx.vstack(
rx.heading("About"),
rx.link("Go home", href="/"),
)
app = rx.App()
app.add_page(home, route="/")
app.add_page(about, route="/about")
Database integration#
Reflex includes SQLModel integration via rx.Model.
import reflex as rx
class Todo(rx.Model, table=True):
id: int | None = None
text: str
done: bool = False
class TodoState(rx.State):
todos: list[Todo] = []
new_text: str = ""
def load_todos(self):
with rx.session() as session:
self.todos = session.exec(Todo.select()).all()
def add_todo(self):
with rx.session() as session:
todo = Todo(text=self.new_text)
session.add(todo)
session.commit()
self.new_text = ""
self.load_todos()
def toggle_done(self, todo_id: int):
with rx.session() as session:
todo = session.get(Todo, todo_id)
todo.done = not todo.done
session.commit()
self.load_todos()
def set_new_text(self, value: str):
self.new_text = value
Deployment#
# Export for self-hosting
reflex export --frontend-only # static files in frontend/
# Or run with production settings
reflex run --env prod
# Deploy to Reflex Cloud (one command)
reflex deploy
Output: (none — exits 0 on success)
Quick reference#
| Task | Code |
|---|---|
| Init project | reflex init |
| Dev server | reflex run |
| State field | class S(rx.State): count: int = 0 |
| Event handler | def increment(self): self.count += 1 |
| Computed var | @rx.var def doubled(self) -> int: return self.count * 2 |
| Bind to event | rx.button("Click", on_click=State.handler) |
| Foreach | rx.foreach(State.items, component_fn) |
| Conditional | rx.cond(State.flag, true_comp, false_comp) |
| Input bind | rx.input(on_change=State.set_field) |
| Form submit | rx.form(..., on_submit=State.handle_submit) |
| Stream updates | async def handler(self): yield between mutations |
| Add page | app.add_page(fn, route="/path") |
| Navigate | rx.link("text", href="/page") or rx.redirect("/page") |
| DB session | with rx.session() as s: s.exec(...) |
| Deploy | reflex deploy |