cheat sheet

reflex

Build interactive web applications entirely in Python with Reflex. Covers state, components, events, pages, database, forms, and deployment.

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

bash
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

python
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

State mutations must happen inside event handlers — you cannot mutate self.field outside an event handler method. Direct assignment in __init__ or other methods is silently ignored or raises an error.

rx.State fields must be typed — untyped fields are not tracked by Reflex's reactivity system. Always annotate: count: int = 0, not count = 0.

reflex run runs 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.

Use rx.var for computed/derived properties that depend on other state fields. They update automatically when their dependencies change, just like React's useMemo.

yield inside 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.

python
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.

python
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.

python
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.

python
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

python
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.

python
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

python
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.

python
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

bash
# 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)

Real-world recipes

End-to-end snippets that show how state, components, async handlers, and routing compose in a real Reflex app.

1. Form with server-side validation

python
import reflex as rx

class SignupState(rx.State):
    name: str = ""
    email: str = ""
    errors: dict[str, str] = {}
    success: bool = False

    def submit(self, form: dict):
        self.errors = {}
        if not form.get("name", "").strip():
            self.errors["name"] = "Name is required"
        if "@" not in form.get("email", ""):
            self.errors["email"] = "Invalid email"
        if self.errors:
            return
        self.name = form["name"].strip()
        self.email = form["email"].strip()
        self.success = True

def signup_form() -> rx.Component:
    return rx.form(
        rx.vstack(
            rx.input(placeholder="Name", name="name"),
            rx.cond(SignupState.errors["name"],
                    rx.text(SignupState.errors["name"], color="red", size="1")),
            rx.input(placeholder="Email", name="email", type="email"),
            rx.cond(SignupState.errors["email"],
                    rx.text(SignupState.errors["email"], color="red", size="1")),
            rx.button("Sign up", type="submit"),
        ),
        on_submit=SignupState.submit,
        reset_on_submit=False,
    )

def signup_page() -> rx.Component:
    return rx.cond(
        SignupState.success,
        rx.callout(f"Welcome, {SignupState.name}!", icon="check"),
        signup_form(),
    )

2. Live chat with streaming response

python
import reflex as rx
import asyncio

class ChatState(rx.State):
    messages: list[dict] = []
    draft: str = ""
    streaming: bool = False

    async def send(self):
        if not self.draft.strip():
            return
        self.messages.append({"role": "user", "text": self.draft})
        prompt = self.draft
        self.draft = ""
        self.streaming = True
        self.messages.append({"role": "bot", "text": ""})
        yield

        # Simulate streaming tokens
        for chunk in ("Hello ", "there! ", "You said: ", f"'{prompt}'"):
            await asyncio.sleep(0.15)
            self.messages[-1]["text"] += chunk
            yield

        self.streaming = False
        yield

def msg_bubble(m) -> rx.Component:
    return rx.box(
        rx.text(m["text"]),
        background_color=rx.cond(m["role"] == "user", "#e0e7ff", "#f3f4f6"),
        padding="8px 12px",
        border_radius="8px",
        margin_y="4px",
    )

def chat_page() -> rx.Component:
    return rx.vstack(
        rx.foreach(ChatState.messages, msg_bubble),
        rx.hstack(
            rx.input(
                value=ChatState.draft,
                on_change=ChatState.set_draft,
                placeholder="Type a message...",
            ),
            rx.button("Send",
                      on_click=ChatState.send,
                      loading=ChatState.streaming),
        ),
        width="100%",
    )

3. CRUD dashboard with SQLModel

python
import reflex as rx

class Note(rx.Model, table=True):
    id: int | None = None
    title: str
    body: str = ""

class NotesState(rx.State):
    notes: list[Note] = []
    title: str = ""
    body: str = ""
    editing: int | None = None

    def load(self):
        with rx.session() as s:
            self.notes = list(s.exec(Note.select()))

    def save(self):
        with rx.session() as s:
            if self.editing:
                n = s.get(Note, self.editing)
                n.title = self.title
                n.body = self.body
            else:
                s.add(Note(title=self.title, body=self.body))
            s.commit()
        self.title = self.body = ""
        self.editing = None
        self.load()

    def edit(self, note_id: int):
        with rx.session() as s:
            n = s.get(Note, note_id)
            self.title = n.title
            self.body = n.body
            self.editing = note_id

    def delete(self, note_id: int):
        with rx.session() as s:
            n = s.get(Note, note_id)
            s.delete(n)
            s.commit()
        self.load()

4. Multi-page app with shared state and layouts

python
import reflex as rx

class GlobalState(rx.State):
    theme: str = "light"
    user: str = "guest"

    def toggle_theme(self):
        self.theme = "dark" if self.theme == "light" else "light"

def layout(content: rx.Component) -> rx.Component:
    return rx.vstack(
        rx.hstack(
            rx.heading("MyApp"),
            rx.spacer(),
            rx.text(f"Theme: {GlobalState.theme}"),
            rx.button("Toggle", on_click=GlobalState.toggle_theme),
            width="100%",
        ),
        content,
        align="stretch",
        padding="2em",
    )

def home() -> rx.Component:
    return layout(rx.vstack(rx.heading("Home"), rx.link("Dashboard", href="/dashboard")))

def dashboard() -> rx.Component:
    return layout(rx.vstack(rx.heading("Dashboard"), rx.link("Home", href="/")))

app = rx.App()
app.add_page(home, route="/")
app.add_page(dashboard, route="/dashboard")

5. Wrapping a custom React component

python
import reflex as rx

class Chart(rx.Component):
    """Wrap a react chart library installed via npm."""
    library = "recharts@2.x"
    tag = "BarChart"

    data: rx.Var[list[dict]]
    width: rx.Var[int] = 400
    height: rx.Var[int] = 300

# Use it in a page
def stats_page() -> rx.Component:
    return Chart(
        data=[{"name": "Jan", "value": 10}, {"name": "Feb", "value": 20}],
        width=600,
        height=400,
    )

Production deployment

Reflex apps have two halves: the Next.js frontend (compiled bundle) and the FastAPI backend (Python websocket server). Deploy them together with reflex run --env prod or split them across CDN + backend.

bash
# Build the frontend bundle and Python wheel
reflex export

# Or build only the frontend (for static hosting + remote backend)
reflex export --frontend-only --no-zip

# Run production stack on one box
REFLEX_DB_URL=postgresql://user:pw@db:5432/app reflex run --env prod \
  --backend-host 0.0.0.0 --backend-port 8000

# Or deploy to Reflex Cloud
reflex deploy --project my-app

Output: (none — exits 0 on success)

rxconfig.py — production config:

python
import reflex as rx

config = rx.Config(
    app_name="my_app",
    db_url="postgresql://user:pw@db:5432/app",
    api_url="https://api.example.com",
    deploy_url="https://app.example.com",
    cors_allowed_origins=["https://app.example.com"],
    telemetry_enabled=False,
    timeout=120,
)

Topology options:

TopologyFrontendBackendBest for
Reflex CloudManagedManagedFastest path to production; no infra
All-in-one containerBundled with backendFastAPISingle VM, single Docker container
Split: CDN + backendS3/CloudFront/VercelEC2/Fly.io/RenderCaching the static bundle; multi-region
Self-hosted K8sService + IngressStatefulSetEnterprise / on-prem

Production checklist:

  1. Pin the Node.js version that ships with Reflex (reflex --version shows it). The frontend build is sensitive to Node minor versions.
  2. Use Postgres in production, not the default SQLite. Each backend instance needs the same DB.
  3. Set cors_allowed_origins explicitly; leaving it open is a security footgun.
  4. Bundle size grows with components — Tailwind purging on the production build cuts ~40% off the JS bundle.
  5. WebSockets need sticky sessions behind a reverse proxy. Configure your load balancer to hash on the user's connection ID.

Performance tuning

Reflex's performance hinges on three things: the frontend bundle size, server-side state-update frequency, and websocket round-trips.

python
# 1. Memoise expensive computed vars — they re-run on every state change
class State(rx.State):
    items: list[dict] = []

    @rx.var(cache=True)        # only re-run when items changes
    def sorted_items(self) -> list[dict]:
        return sorted(self.items, key=lambda x: x["score"], reverse=True)

# 2. Batch state updates — one yield per logical step, not per field
class BadState(rx.State):
    a: int = 0
    b: int = 0

    async def update_both(self):
        self.a = 1
        yield   # round-trip 1
        self.b = 2
        yield   # round-trip 2

class GoodState(rx.State):
    a: int = 0
    b: int = 0

    async def update_both(self):
        self.a = 1
        self.b = 2
        yield   # single round-trip

# 3. Avoid huge lists in state — page server-side instead
class PagedState(rx.State):
    page: int = 0
    page_size: int = 50

    @rx.var(cache=True)
    def visible_items(self) -> list[dict]:
        with rx.session() as s:
            return list(s.exec(
                Item.select().offset(self.page * self.page_size).limit(self.page_size)
            ))

Output: (none — exits 0 on success)

Tuning checklist:

  1. Memoise computed vars with @rx.var(cache=True) for any expensive derivation.
  2. Minimise the number of yields in async handlers — each is a websocket message.
  3. Don't put 10k-item lists in state. Paginate server-side; only ship the visible window.
  4. Use rx.foreach over Python comprehensions when rendering lists — foreach diffs efficiently.
  5. Lazy-load routes with app.add_page(..., on_load=State.fetch) rather than fetching at module import.
  6. Disable telemetry in production (telemetry_enabled=False) — saves a network call on every page load.

Testing patterns

Reflex's testing story is still maturing. The most reliable approach is to unit-test state classes as plain Python objects and end-to-end-test the rendered app with Playwright.

python
import pytest
from my_app.state import SignupState

# 1. Unit-test state methods — instantiate without the framework
def test_submit_invalid_email_sets_error():
    s = SignupState()
    s.submit({"name": "Alice", "email": "bad"})
    assert "email" in s.errors
    assert not s.success

def test_submit_valid_succeeds():
    s = SignupState()
    s.submit({"name": "Alice", "email": "alice@example.com"})
    assert s.success
    assert s.name == "Alice"

# 2. Computed vars — call the underlying method
def test_item_count():
    s = SignupState()
    s.name = "Alice"
    # Computed vars decorated with @rx.var still work as descriptors

Output: (none — exits 0 on success)

python
# 3. End-to-end with Playwright
# tests/test_e2e.py
import pytest
from playwright.sync_api import Page

def test_signup_flow(page: Page, reflex_app_url: str):
    page.goto(reflex_app_url)
    page.fill('input[name="name"]', "Alice Dev")
    page.fill('input[name="email"]', "alice@example.com")
    page.click('button[type="submit"]')
    page.wait_for_selector("text=Welcome, Alice Dev!")

Reflex's built-in pytest helpers are limited. As of the current stable release, there is no first-class equivalent to FastAPI's TestClient for state handlers. Test business logic on plain state objects, and exercise the UI through Playwright or a manual harness.

Migration from older Reflex versions

Reflex (originally Pynecone) went 1.0 in mid-2024 and renamed in the process. Older code uses the pc import alias and several renamed APIs.

ConceptPynecone / pre-1.0Reflex 1.x
Moduleimport pynecone as pcimport reflex as rx
Configpcconfig.pyrxconfig.py
State basepc.Staterx.State
Componentpc.Componentrx.Component
Run commandpc runreflex run
Init commandpc initreflex init
Conditionalpc.condrx.cond
Looppc.foreachrx.foreach
Computed var@pc.var@rx.var
DB sessionpc.session()rx.session()

Migration steps:

  1. Rename pcconfig.pyrxconfig.py and update app_name.
  2. Run a project-wide search-replace for pc.rx. and import pynecone as pcimport reflex as rx.
  3. Refresh the Node bundle: delete .web/ and assets/external/ directories, then reflex run to regenerate.
  4. Re-pin extra component packages — Pynecone's pc.NextLink etc. are now rx.next.link.
  5. State subclass deprecations. Pre-1.0 allowed untyped fields; 1.x requires type annotations.

Ecosystem integrations

Reflex's component library wraps the most-used React libraries. Custom integrations are straightforward via rx.Component subclassing.

LibraryWrapped as
Radix UIBuilt-in — most rx.* primitives are Radix wrappers
Tailwind CSSBuilt-in; pass class_name="..." to any component
Rechartsrx.recharts.line_chart(...), rx.recharts.bar_chart(...)
SQLModelrx.Model(..., table=True) + rx.session()
FastAPIReflex backend IS FastAPI; mount endpoints on app.api
Chakra UIAvailable as a community plugin
Any npm React componentclass MyComp(rx.Component): library = "pkg@1.x"; tag = "Comp"

Patterns & idioms

  • State splitting. Subclass rx.State per page or feature: UserState, OrdersState. Avoid one mega-state.
  • @rx.var(cache=True) for derived data that's expensive to compute.
  • rx.foreach over list comprehensions when rendering lists in JSX — foreach diffs efficiently.
  • yield for streaming state updates inside async handlers.
  • on_load=State.fetch to load data when a route is entered, not at app boot.
  • Layouts as functions, not classes. def layout(content): return rx.vstack(navbar(), content).
  • rx.fragment for grouping without a wrapper DOM element when CSS layout requires direct parenting.

Troubleshooting common errors

ErrorCauseFix
State field 'count' must be typedMissing annotationUse count: int = 0, not count = 0
Cannot mutate state outside of an event handlerDirect assignment in __init__ or renderMove to a method decorated as event handler
Component prop X does not accept type YMismatched prop typeCast in Python or use rx.Var.create(...)
First reflex run takes 60+ secondsInitial Node bundle compileExpected; subsequent runs are cached in .web/
EADDRINUSE :3000Old dev server still runningpkill -f reflex or change port in rxconfig.py
WebSocket disconnects in productionReverse proxy not configured for upgradeSet Upgrade and Connection headers in nginx/Caddy
rx.foreach only renders first itemArgument is not an rx.Var listMake sure the list is a state attribute, not a Python local
Computed var doesn't updateMissing cache=True or dependency not trackedRead dependencies explicitly inside the var body
Static assets 404 in prodassets/ not copied into imageInclude assets/ in your Dockerfile

When NOT to use this

Reflex is excellent for full-stack Python apps but is not the right choice for every UI.

  • Heavy client-side interactivity. Drag-drop, canvas-heavy editors, or 60fps animations need a real React app. Reflex's server-round-trip model adds latency.
  • Public SEO-critical marketing sites. Reflex SSRs the first page only; for deep static content use Astro or Next.js.
  • Mobile-first PWAs with offline support. Reflex requires a live websocket; offline-first apps need a service worker and local state.
  • Embedding into an existing React app. Reflex is a complete framework, not a component library; integration is non-trivial.
  • When you don't know Python well. Reflex's abstraction hides a lot — debugging needs comfort with both React internals and the Reflex runtime.
  • Lightweight dashboards or notebooks. Streamlit is simpler if you just need to display data; Reflex's reactivity model is overkill.

Quick reference

TaskCode
Init projectreflex init
Dev serverreflex run
State fieldclass S(rx.State): count: int = 0
Event handlerdef increment(self): self.count += 1
Computed var@rx.var def doubled(self) -> int: return self.count * 2
Bind to eventrx.button("Click", on_click=State.handler)
Foreachrx.foreach(State.items, component_fn)
Conditionalrx.cond(State.flag, true_comp, false_comp)
Input bindrx.input(on_change=State.set_field)
Form submitrx.form(..., on_submit=State.handle_submit)
Stream updatesasync def handler(self): yield between mutations
Add pageapp.add_page(fn, route="/path")
Navigaterx.link("text", href="/page") or rx.redirect("/page")
DB sessionwith rx.session() as s: s.exec(...)
Deployreflex deploy