Reflex State-Architektur: die Muster, die funktionierend von fragil trennen

State-Graph-Design, async Events, Datenbankintegration und WebSocket-Deployment — die Entscheidungen, die bestimmen, ob eine Reflex-App wächst oder stagniert.

open-source-knowledge

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:

  1. 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.
  2. 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.
  3. 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.

Mehr Lesen

Kontaktformular

Schreiben Sie uns kurz, worum es geht. Wir melden uns in der Regel innerhalb eines Werktags.

Christian Wörle

Ihr Ansprechpartner

Christian Wörle

Technical Lead

contact@devolute.org