How Reflex works in 5 minutes¶
If you've used Reflex before, skim this and move on. If you've only ever written Django (or your usual stack is FastAPI / Flask / something server-rendered), this page is for you. By the end you'll know enough Reflex vocabulary to read the rest of these docs without looking things up.
The big idea¶
Reflex lets you build a web UI in pure Python. You don't write React, you don't write JSX, you don't write CSS in a separate folder. You write Python functions that return components, and Python classes that hold state. Reflex compiles all of that to a real React app.
So when a user opens your site, two things are happening:
- The browser loads a compiled SPA (single-page app — a folder of HTML/JS that Reflex generated from your code).
- The SPA opens a WebSocket to your server. From then on, every click, every form change, every navigation flows over that one connection.
First load Every interaction after
────────── ───────────────────────
HTML/JS WebSocket
─────────► ◄─────────►
from server clicks go up, state diffs come down
A WebSocket is just one persistent connection. Instead of making a new HTTP request for every action, the browser and server keep talking on the same line.
Components — functions that return UI¶
A Reflex component is a Python function that returns an rx.Component:
import reflex as rx
def hello() -> rx.Component:
return rx.vstack(
rx.heading("Hello!"),
rx.text("Welcome to my app."),
rx.button("Click me"),
)
rx.vstack, rx.heading, rx.text, rx.button are component builders. They look like HTML elements but they're plain Python. You compose them by passing them as arguments.
That's the whole component layer. Functions in, components out.
State — Python classes that hold your app's data¶
If components are like HTML, state is like the JavaScript that backs them. A state is a Python class that inherits from rx.State:
Two things to notice:
count: int = 0is a reactive variable. When you change it on the server, the UI updates automatically.incrementis decorated with@rx.event. That means it's an event handler — a method the client can call.
Wire it to a component:
def counter_ui() -> rx.Component:
return rx.vstack(
rx.text(f"Count: {CounterState.count}"),
rx.button("Add one", on_click=CounterState.increment),
)
Click the button → the browser sends an event over the WebSocket → increment runs on the server → self.count changes → Reflex sends the new value back → the text updates. You wrote no JavaScript.
Pages — components that get a URL¶
A page is just a component decorated with @rx.page (or, in reflex-django, @template):
Visit /counter and you see the page. The client-side router handles navigation between pages without a full reload.
In reflex-django, you put these page functions directly inside your Django app's views.py:
# shop/views.py
from reflex_django import template
@template(route="/", title="Home")
def home() -> rx.Component:
return rx.heading("Hello")
That's the same idea as @rx.page, just with a small default layout wrapper around your content.
The event loop, end to end¶
Here's what actually happens when a user clicks a button:
1. Browser: user clicks "Add one"
2. Browser: send event { handler: "CounterState.increment", args: [] } over WebSocket
3. Server: Reflex finds the CounterState instance for this user/tab
4. Server: runs CounterState.increment() (self.count += 1)
5. Server: notices count changed, sends { CounterState.count: 1 } back
6. Browser: React re-renders; "Count: 0" becomes "Count: 1"
You only wrote step 4. Reflex handled the rest.
Async event handlers¶
Event handlers can be async def. This matters a lot for reflex-django because Django's async ORM methods need an async context:
class TaskState(rx.State):
tasks: list[dict] = []
@rx.event
async def load(self):
# async iteration over the database
self.tasks = [
{"title": t.title}
async for t in Task.objects.all()
]
Use async def whenever you talk to the database.
The compiled SPA — built once, served from disk¶
Reflex doesn't run your Python in the browser. Before your app can serve users, Reflex compiles your Python into a real React app and writes it to disk. There's an .web/ folder where the source lives and a built bundle that gets shipped.
In reflex-django:
python manage.py run_reflexrebuilds the bundle and serves it through Django.- In production, you run
python manage.py export_reflexin CI and Django serves the static files fromSTATIC_ROOT/_reflex/.
You don't have to think about this most of the time. Just know that something is compiled, so when you change a Python file, that compile step runs again.
Where Reflex stops, and where the gap lives¶
Notice what's not in this story:
- No
HttpRequest. The WebSocket payload has the path, query string, and a few headers — but no real Django request object. - No middleware. Reflex doesn't know what
settings.MIDDLEWAREis. - No
request.user. Reflex has its own state, but it doesn't know about Django auth. - No
csrf_token, nomessages, norequest.session.
That's why every @rx.event handler in plain Reflex is "blind" to your Django world. The data is right there in the cookies — but nothing inside Reflex unpacks it.
This is the gap reflex-django closes. It rebuilds a real HttpRequest for each event, runs your full middleware chain, and hands the result to your handler as self.request, self.user, self.session, and friends.
You now know enough Reflex¶
If the four bullets below feel obvious, you're ready for the rest of these docs:
- A Reflex app is a compiled SPA that talks to the server over a WebSocket.
- Components are Python functions returning
rx.Component. - State is a Python class with reactive fields and
@rx.eventmethods. - Pages are components with a
@rx.page(or@template) decorator and aroute.
The full official Reflex docs live at reflex.dev. For our purposes, the page above is plenty.