skip to content

reflex — Full-Stack Web Apps in Pure Python

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

6 min read 11 snippets deep dive

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.field outside an event handler method. Direct assignment in __init__ or other methods is silently ignored or raises an error.

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

[!WARNING] 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.

[!TIP] 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.

[!TIP] 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.

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#

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