Zurück zu allen Beiträgen
·5 Min. Lesezeit

Asynchrone React-Oberflächen als Zustandsmaschinen gestalten

Wenn ein React-Bildschirm Daten von einem Server lädt, beginnt sein erstes Design oft mit einigen Booleans: isLoading, hasError, vielleicht isSaving. Mit wachsendem Bildschirm beginnen diese Flags, sich zu widersprechen. Sind Daten sichtbar, während sie im Hintergrund aktualisiert werden? Ist die erste Anfrage fehlgeschlagen oder trat ein Fehler auf, während die Person vorhandene Daten bearbeitet? Sollten ein leeres Ergebnis, fehlende Berechtigung und ein vorübergehender Netzwerkfehler gleich aussehen?

Diese Fragen werden nicht wegen einer Komponente schwierig. Das eigentliche Problem ist, dass die möglichen Zustände der Oberfläche nie explizit modelliert wurden. Ein Zustandsmaschinen-Ansatz behandelt nutzersichtbare Zustände und Übergänge zwischen ihnen als Vertrag. Das Ziel ist nicht, ein akademisches Diagramm zu zeichnen; es geht darum, unmögliche Kombinationen aus dem Code zu entfernen und die Nutzererfahrung vorhersehbar zu machen.

Warum Boolean-Flags nicht mehr ausreichen

Unabhängige Flags erlauben Kombinationen, die fachlich keinen Sinn ergeben. Zum Beispiel können isLoading: true, data: [] und error gleichzeitig existieren. Es ist unklar, welchem Bildschirm das entspricht. Ein Entwickler kann einen Spinner anzeigen, ein anderer ein Fehlerpanel und ein dritter die Empty-State-Komponente.

Das Problem ist nicht nur visuell. Jedes zusätzliche Flag erhöht die Zahl der zu testenden Kombinationen. Drei Booleans erzeugen acht Möglichkeiten, fünf erzeugen zweiunddreißig. Die meisten haben keine Produktbedeutung, trotzdem betrachtet der Code sie als möglich.

Sicherer ist es, Zustände als benannte, gegenseitig ausschließende Optionen zu schreiben:

type ResourceState<T> =
  | { kind: "idle" }
  | { kind: "loading" }
  | { kind: "ready"; data: T; isRefreshing: boolean }
  | { kind: "empty" }
  | { kind: "error"; message: string; canRetry: boolean };

In diesem Modell kann eine Ressource nicht gleichzeitig empty und ready sein. Auch der Zustand „Daten sind sichtbar und werden im Hintergrund aktualisiert“ verschwindet nicht; er ist eine explizite Eigenschaft von ready.

Zuerst nutzersichtbare Zustände auflisten

Bevor Code geschrieben wird, hilft es, den Weg des Bildschirms in Alltagssprache zu beschreiben. Eine Listenseite kann diese Zustände benötigen:

  • initiales Laden, solange noch kein Inhalt vorhanden ist,
  • ein erfolgreiches Ergebnis mit Einträgen,
  • ein erfolgreiches Ergebnis ohne Einträge,
  • stille Aktualisierung, während vorhandene Daten sichtbar bleiben,
  • ein erneut versuchbarer Fehler,
  • ein Zustand mit benötigter Berechtigung oder entzogenem Zugriff,
  • ein vom Nutzer gestarteter Speichervorgang.

Diese Liste zeigt, dass ein leerer Zustand kein Fehler ist und eine Aktualisierung nicht wie das erste Laden wirken sollte. Für jeden Zustand sollte die nächste mögliche Aktion notiert werden: warten, erneut versuchen, einen Filter entfernen, sich anmelden oder weiter bearbeiten.

Zustände in Produktsprache zu benennen, ist wichtig. Namen wie refreshing, permissionDenied oder savingDraft erleichtern Gespräche zwischen Design, Entwicklung und Test stärker als phase2 oder isBusy.

Übergänge mit Ereignissen definieren

Eine Zustandsmaschine ist mehr als eine Liste von Zuständen; sie beschreibt auch, welches Ereignis welchen Übergang erzeugen darf. Eine erfolgreiche Antwort während loading führt zu ready, wenn sie Einträge enthält, oder zu empty, wenn nicht. Eine in ready angeforderte Aktualisierung setzt isRefreshing: true, ohne sichtbare Daten zu entfernen. Scheitert diese Aktualisierung, bleiben die alten Daten erhalten und der Nutzer erhält eine kleine Rückmeldung.

Diese Unterscheidung ist bei Netzwerklatenz wertvoll. Niemand sollte auf einen Vollbild-Spinner warten müssen, um eine Liste wiederzusehen, die bereits sichtbar war. Ebenso darf eine späte Antwort einer alten Anfrage nicht das Ergebnis eines neueren Filters überschreiben. Eine Anfrage-ID, ein Abbruchsignal oder die Prüfung aktueller Parameter stellt sicher, dass nur eine gültige Antwort den Zustand ändern kann.

Diese Regeln können in einem Reducer, einem Custom Hook oder dem Zustandsobjekt einer Datenabruf-Bibliothek liegen. Unabhängig vom Werkzeug ist entscheidend, dass Übergänge an einer Stelle lesbar sind.

Ressourcen- und Mutationszustand trennen

Eine Liste zu laden und einen Datensatz zu speichern haben nicht denselben Lebenszyklus. Wenn ein einziges isLoading-Flag beides darstellt, kann ein gesamter Bildschirm unnötig gesperrt werden, während eine Person ein Formular absendet. Ressourcen und Nutzeraktionen sollten stattdessen getrennt modelliert werden.

Ein Profil kann beispielsweise ready bleiben, während eine Speichern-Schaltfläche submitting, succeeded und failed durchläuft. Gehört ein Fehler zu einem Formularfeld, erscheint er neben diesem Feld. Gibt es ein allgemeineres Netzwerkproblem, bleiben die Formulardaten erhalten und es wird eine Wiederholung angeboten. Dieses Design versteckt den Kontext des Fehlers nicht.

Bei einer optimistischen Aktualisierung muss auch der Rückrollpfad Teil des Übergangsmodells sein. Eine Zahl kann sofort steigen, nachdem jemand eine Reaktion hinzufügt; wird die Anfrage abgelehnt, muss der vorherige Wert zurückkehren und bei Bedarf eine Erklärung erscheinen. Die Annahme von Erfolg beseitigt den Fehlerpfad nicht.

Tests von Screenshots auf Übergänge verlagern

Der praktischste Gewinn eines Zustandsmaschinen-Ansatzes liegt im Testen. Statt jeden Zustand durch zufällige UI-Bedingungen zu erzeugen, lässt sich die Frage testen: „Welcher Zustand soll diesem Ereignis folgen?“ Leere Ergebnisse, Wiederholungen, zwei aufeinanderfolgende Aktualisierungen und verspätete Antworten lassen sich unabhängig vom Komponentenbaum prüfen.

Komponententests bleiben wertvoll, sie sollten aber prüfen, wie ein Zustand für Nutzer aussieht. Ein Übergangstest beweist, dass der Zustand selbst korrekt ist. Diese Trennung sichert Abläufe ab, die Nutzer selten sehen, die aber bei Fehlern kritisch werden.

Nicht jeder Bildschirm braucht eine vollständige Maschine

In Zustandsmaschinen zu denken bedeutet nicht, für jeden Button eine große Bibliothek oder ein visuelles Diagramm zu verwenden. Eine einfache Interaktion braucht vielleicht nur zwei gut benannte Zustände. Komplexität entsteht bei parallelen Anfragen, Rollback, Entwürfen, Berechtigungen, mehrstufigen Formularen oder Echtzeit-Updates.

Die Prüfung ist einfach: Begegnet das Produkt einem Bildschirmzustand, der einem Nutzer schwer zu erklären ist? Wenn ja, ist das Benennen von Zuständen und Übergängen meist besser als ein weiteres Boolean. Eine Oberfläche kann nicht jede Netzwerkunsicherheit entfernen, aber sie kann Unsicherheit für Nutzer verständlich und für das Team testbar machen.