Reflex nimmt eine andere Position ein als Streamlit oder Gradio. Es ist kein Dashboard-Tool oder Modell-Demo-Framework — es ist ein Full-Stack-Web-Framework, das Python zu React kompiliert und State-Änderungen über WebSockets kommuniziert. Der korrekte Vergleich ist Next.js, SvelteKit oder Django mit HTMX, nicht Streamlit.
Das bedeutet, dass auch die Engineering-Aspekte andere sind: State-Graph-Design, Event-Handler-Isolation, Datenbankintegration, Background-Tasks und WebSocket-bewusstes Deployment spielen in einer Weise eine Rolle, die bei Streamlit oder Gradio nicht anfällt.
Wie Reflex State funktioniert
Reflex-State ist eine Python-Klasse, die von rx.State erbt. Felder der Klasse sind reaktiv: wenn ein Event-Handler ein Feld mutiert, vergleicht Reflex alten und neuen State, serialisiert das Delta und pusht es über einen WebSocket an den Browser. Das React-Frontend wendet das Delta an, ohne vollständig neu zu rendern.
class AppState(rx.State):
items: list[str] = []
loading: bool = False
async def add_item(self, item: str):
self.loading = True
yield # loading=True sofort an den Client flushen
self.items.append(item)
self.loading = False
Das yield innerhalb eines Event-Handlers ist bedeutsam: es flusht das aktuelle State-Delta an den Client, ohne den Handler zu beenden. Das ermöglicht Fortschrittsanzeigen, gestreamte Teilergebnisse und jede Interaktion, die Zwischen-UI-Updates während einer langen Operation braucht.
State-Graph-Design: was wohin gehört
Der häufigste Reflex-Fehler: alles in eine einzige State-Klasse packen. Mit wachsender App entsteht ein State-Objekt, das groß ist (alles wird pro Verbindung serialisiert), langsam zu vergleichen (jedes Event berechnet Diffs über den gesamten State) und unmöglich isoliert zu testen ist.
Das korrekte Muster: mehrere State-Klassen mit expliziten Substates.
class AuthState(rx.State):
user_id: str = ""
user_role: str = ""
class DashboardState(AuthState):
data: list[dict] = []
filter_value: str = ""
class SettingsState(AuthState):
theme: str = "light"
notifications_enabled: bool = True
DashboardState erbt AuthState, hat also Zugang zu user_id und user_role, ohne sie zu duplizieren. Reflex serialisiert jeden Substate unabhängig und vergleicht nur den betroffenen Substate bei jedem Event. Eine Filteränderung in DashboardState erzwingt keinen Diff von SettingsState.
Regeln für State-Graph-Design:
- UI-only-State wenn möglich auf Komponentenebene halten. Reflex 0.4+ unterstützt lokalen Komponenten-State via
rx.ComponentState. Verwenden Sie ihn für Toggle offen/geschlossen, Tab-Auswahl und jeden State, der nicht geteilt werden muss. - Geteilten State in den nächsten gemeinsamen Vorfahren. Wenn zwei Komponenten auf verschiedenen Seiten denselben Wert brauchen, gehört er in einen gemeinsamen Eltern-State.
- Backend-only-Werte vollständig aus dem State heraushalten. Datenbankverbindungen, API-Clients und Modell-Weights gehören nicht in
rx.State. Sie kommen in Modul-level-Singletons oder Dependency-Injection-Muster.
Async Events und Background-Tasks
Reflex-Event-Handler können async sein. Für Operationen, die mehr als ein paar Millisekunden dauern — Datenbankabfragen, API-Calls, Modell-Inferenz — ist async Pflicht:
async def load_data(self):
self.loading = True
yield
async with httpx.AsyncClient() as client:
response = await client.get(self.api_url)
self.data = response.json()
self.loading = False
Für Operationen, die den UI-Thread überhaupt nicht blockieren sollen — E-Mails versenden, Reports ausführen, Uploads verarbeiten — rx.background verwenden:
@rx.background
async def process_upload(self, file_data: bytes):
result = await heavy_processing(file_data)
async with self: # State-Lock wieder erlangen, um zu mutieren
self.result = result
self.processing = False
rx.background führt den Handler als Background-Task aus. Der async with self-Block erlangt den State-Lock wieder, um State sicher von außerhalb des normalen Event-Dispatch-Zyklus zu mutieren. Das ist notwendig, weil Reflex-State nicht thread-sicher ist — alle Mutationen müssen durch den State-Lock.
Datenbankintegration
Reflex enthält eine eingebaute SQLAlchemy-Integration via rx.Model. Für einfache CRUD-Applikationen ist das praktisch. Für alles jenseits von einfachem CRUD — komplexe Queries, async Treiber, auf die Last abgestimmtes Connection-Pooling — SQLAlchemy direkt mit einem async Engine verwenden:
engine = create_async_engine(DATABASE_URL, pool_size=10, max_overflow=5)
async def get_users_by_role(role: str) -> list[dict]:
async with AsyncSession(engine) as session:
result = await session.execute(
select(User).where(User.role == role)
)
return [u.__dict__ for u in result.scalars()]
Aus Event-Handlern aufrufen. Die Session wird pro Call erstellt und geschlossen, was für Web-Request-Muster korrekt ist. Keine Session in rx.State speichern — SQLAlchemy-Sessions sind nicht serialisierbar.
WebSocket-Deployment
Reflex kommuniziert via WebSocket. Das hat Deployment-Implikationen, die sich von einer Standard-HTTP-API unterscheiden:
Sticky Sessions sind für Multi-Instance-Deployments erforderlich. Jede Reflex-Verbindung pflegt Server-seitigen State, der an einen bestimmten Prozess gebunden ist. Ein Load-Balancer, der Requests Round-Robin zwischen Instanzen verteilt, bricht aktive Verbindungen. Cookie-basierte Session-Affinity in nginx, AWS ALB oder GCP Cloud Run konfigurieren.
WebSocket-Timeout-Konfiguration. Standard-nginx-Proxy-Timeouts (60s) terminieren idle WebSocket-Verbindungen. proxy_read_timeout auf einen Wert länger als die längste erwartete Idle-Periode setzen (z.B. proxy_read_timeout 3600s; für Anwendungen, wo Nutzer einen Tab offen lassen können).
Frontend und Backend deployen atomar. Reflex kompiliert das Frontend zum Build-Zeitpunkt. Ein Mismatch zwischen dem Frontend-JavaScript und dem Backend-Python-State-Protokoll verursacht subtile Bugs — State-Updates, die zu funktionieren scheinen, aber die UI korrumpieren. Die CI/CD-Pipeline muss beides zusammen deployen, nie unabhängig.
Redis für State in Multi-Instance-Setups. Standardmäßig speichert Reflex State im Prozess. Für horizontale Skalierung ein Redis-State-Backend konfigurieren. Das verlagert State aus dem Prozess und erlaubt mehreren Reflex-Instanzen, ihn zu teilen — fügt aber auch Serialisierungs-Overhead pro Event hinzu. Benchmarken, bevor angenommen wird, dass Redis Multi-Instance-Reflex unkompliziert macht.
Wofür Reflex nicht ausgelegt ist
- SEO-kritische öffentliche Seiten. Reflex rendert client-seitig. Suchmaschinen-Crawler sehen eine weitgehend leere HTML-Hülle. Wenn organische Suche wichtig ist, Marketing-Seiten auf einer statischen Site oder einem SSR-Framework hosten und Reflex für die authentifizierte Anwendungsshell verwenden.
- Hochfrequente Echtzeit-Daten. WebSocket ist das richtige Transport-Protokoll, aber Reflexs State-Diff-Modell ist für Nutzerinteraktions-Frequenz optimiert (Klicks, Formular-Absenden), nicht für Sensor-Daten oder Trading-Feeds, die hunderte Male pro Sekunde aktualisieren.
- Große File-Uploads oder Downloads. Diese über einen Signed URL zu S3/GCS routen statt über die Reflex-WebSocket-Verbindung.
Reflex-State-Architektur entwerfen, die skaliert
Wir designen State-Graphen, Datenbankintegration und Deployment — bevor Komplexität die App schwer änderbar macht.