React-Zustandsverwaltung einfach gemacht: Trenne Serverzustand vom Clientzustand, befolge ein paar Regeln und erkenne früh Anzeichen wachsender Komplexität.

Zustand ist jede Daten, die sich während der Laufzeit deiner App ändern kann. Dazu gehört, was du siehst (ein Modal ist offen), was du bearbeitest (ein Form-Entwurf) und Daten, die du holst (eine Liste von Projekten). Das Problem ist: all das wird „Zustand“ genannt, obwohl es sich sehr unterschiedlich verhält.
Die meisten chaotischen Apps gehen auf dieselbe Weise kaputt: zu viele Zustandsarten werden am selben Ort vermischt. Eine Komponente hält plötzlich Serverdaten, UI-Flags, Form-Entwürfe und abgeleitete Werte und versucht, sie mit Effekten synchron zu halten. Nach kurzer Zeit kannst du einfache Fragen wie „woher kommt dieser Wert?“ oder „wer aktualisiert ihn?“ nicht mehr beantworten, ohne mehrere Dateien zu durchsuchen.
Generierte React-Apps rutschen schneller in dieses Verhalten, weil es leicht ist, die erste funktionierende Version zu akzeptieren. Du fügst einen neuen Bildschirm hinzu, kopierst ein Muster, behebst einen Bug mit einem weiteren useEffect und jetzt hast du zwei Wahrheiten. Wenn der Generator oder das Team unterwegs die Richtung ändert (lokaler Zustand hier, globaler Store dort), sammelt der Codebase Muster statt auf einem aufzubauen.
Das Ziel ist langweilig: weniger Arten von Zustand und weniger Orte, an denen man suchen muss. Wenn es ein offensichtliches Zuhause für Serverdaten und ein offensichtliches Zuhause für rein UI-bezogenen Zustand gibt, werden Bugs kleiner und Änderungen fühlen sich weniger riskant an.
„Langweilig bleiben“ heißt, dass du dich an ein paar Regeln hältst:
Ein konkretes Beispiel: Wenn eine Benutzerliste vom Backend kommt, behandle sie als Serverzustand und fetch sie dort, wo sie gebraucht wird. Wenn selectedUserId nur existiert, um ein Detail-Panel zu steuern, behalte es als kleinen UI-Zustand in der Nähe dieses Panels. Das Mischen dieser beiden ist, wie Komplexität beginnt.
Die meisten React-Zustandsprobleme beginnen mit einer Verwechslung: Serverdaten wie UI-Zustand behandeln. Trenne sie früh, und die Zustandsverwaltung bleibt ruhig, selbst wenn deine App wächst.
Serverzustand gehört dem Backend: Nutzer, Bestellungen, Aufgaben, Berechtigungen, Preise, Feature-Flags. Er kann sich ändern, ohne dass deine App etwas tut (ein anderes Tab aktualisiert ihn, ein Admin ändert ihn, ein Job läuft, Daten laufen ab). Weil er geteilt und änderbar ist, brauchst du Fetching, Caching, Refetching und Fehlerbehandlung.
Clientzustand ist das, was nur dein UI gerade interessiert: welches Modal offen ist, welcher Tab ausgewählt ist, ein Filtertoggle, Sortierreihenfolge, eine eingeklappte Sidebar, ein Entwurf einer Suchanfrage. Wenn du den Tab schließt, ist es in Ordnung, es zu verlieren.
Ein schneller Test ist: „Könnte ich die Seite neu laden und das aus dem Server wieder aufbauen?“
Es gibt auch abgeleiteten Zustand, der dich davor bewahrt, zusätzlichen Zustand zu schaffen. Das ist ein Wert, den du aus anderen Werten berechnen kannst, also speicherst du ihn nicht. Gefilterte Listen, Summen, isFormValid und „zeige leere Ansicht“ gehören meist hierhin.
Beispiel: Du holst eine Liste von Projekten (Serverzustand). Der gewählte Filter und das Flag fürs „Neues Projekt“-Dialog sind Clientzustand. Die sichtbare Liste nach dem Filtern ist abgeleiteter Zustand. Wenn du die sichtbare Liste separat speicherst, gerät sie aus dem Tritt und du jagst Bugs wie „warum ist das veraltet?“.
Diese Trennung hilft, wenn ein Werkzeug wie Koder.ai Screens schnell generiert: halte Backend-Daten in einer Fetching-Schicht, UI-Entscheidungen nahe an den Komponenten und vermeide das Speichern berechneter Werte.
Zustand wird schmerzhaft, wenn ein Datenelement zwei Besitzer hat. Der schnellste Weg, es einfach zu halten, ist zu entscheiden, wer was besitzt, und sich daran zu halten.
Beispiel: Du holst eine Liste von Nutzern und zeigst Details an, wenn einer ausgewählt ist. Ein häufiger Fehler ist, das vollständige ausgewählte Nutzerobjekt im Zustand zu speichern. Speichere stattdessen selectedUserId. Behalte die Liste im Server-Cache. Die Detail-Ansicht schaut den Nutzer per ID nach, sodass Refetches die UI ohne zusätzlichen Sync-Code aktualisieren.
In generierten React-Apps ist es außerdem leicht, „hilfreichen“ generierten Zustand zu akzeptieren, der Serverdaten dupliziert. Wenn du Code siehst, der fetch -> setState -> edit -> refetch macht, stopp kurz. Das ist oft ein Zeichen, dass du eine zweite Datenbank im Browser aufbaust.
Serverzustand sind alles, was im Backend lebt: Listen, Detail-Seiten, Suchergebnisse, Berechtigungen, Zähler. Der langweilige Ansatz ist, ein Tool dafür zu wählen und dabei zu bleiben. Für viele React-Apps reicht TanStack Query.
Das Ziel ist einfach: Komponenten fragen nach Daten, zeigen Lade- und Fehlerzustände und kümmern sich nicht darum, wie viele Fetch-Aufrufe unter der Haube stattfinden. Das ist in generierten Apps wichtig, weil kleine Inkonsistenzen sich schnell multiplizieren, wenn neue Screens hinzukommen.
Behandle Query-Keys wie ein Namenssystem, nicht als Nachgedanken. Halte sie konsistent: stabile Array-Keys, nimm nur Inputs auf, die das Ergebnis verändern (Filter, Page, Sort) und bevorzuge ein paar vorhersehbare Formen statt vieler Einzelstücke. Viele Teams legen Key-Building in kleine Helferfunktionen, damit jeder Bildschirm dieselben Regeln nutzt.
Für Writes nutze Mutations mit expliziter Success-Logik. Eine Mutation sollte zwei Fragen beantworten: was hat sich geändert, und was soll die UI als Nächstes tun?
Beispiel: Du erstellst eine neue Aufgabe. Bei Erfolg invalidiere entweder die Tasks-List-Query (damit sie einmal neu lädt) oder mache ein gezieltes Cache-Update (füge die neue Aufgabe in die gecachte Liste ein). Wähle pro Feature eine Strategie und bleib dabei.
Wenn du versucht bist, refetch an mehreren Stellen hinzuzufügen „nur um sicherzugehen“, wähle einen einzigen langweiligen Schritt stattdessen:
Clientzustand ist das, was der Browser besitzt: ein Sidebar-Open-Flag, eine ausgewählte Zeile, Filtertext, ein Draft bevor du speicherst. Halte es nah am Ort der Nutzung, dann bleibt es meist überschaubar.
Fang klein an: useState in der nächsten Komponente. Wenn du Screens generierst (zum Beispiel mit Koder.ai), ist es verführerisch, alles in einen globalen Store zu schieben „nur für den Fall“. So endet ein Store, den niemand versteht.
Verschiebe Zustand nur dann nach oben, wenn du das Sharing-Problem benennen kannst.
Beispiel: Eine Tabelle mit einem Details-Panel kann selectedRowId in der Tabelle halten. Wenn eine Toolbar an einer anderen Stelle der Seite es auch braucht, hebe es zur Page-Komponente. Wenn eine separate Route (wie Bulk Edit) es braucht, ist ein kleiner Store sinnvoll.
Wenn du einen Store (Zustand oder ähnlich) nutzt, halte ihn auf eine Aufgabe fokussiert. Speicher das „Was“ (ausgewählte IDs, Filter), nicht die „Ergebnisse“ (sortierte Listen), die du ableiten kannst.
Wenn ein Store zu wachsen beginnt, frag: ist das noch ein Feature? Wenn die ehrliche Antwort „irgendwie“ ist, teile ihn jetzt auf, bevor das nächste Feature ihn in einen Zustand verwandelt, den du nicht anfassen willst.
Form-Bugs entstehen oft durch das Mischen von drei Dingen: was der Nutzer tippt, was auf dem Server gespeichert ist und was die UI zeigt.
Für langweilige Zustandsverwaltung behandle das Formular als Clientzustand bis zum Absenden. Serverdaten sind die zuletzt gespeicherte Version. Das Formular ist ein Draft. Bearbeite das Serverobjekt nicht direkt. Kopiere Werte in Draft-State, lass den Nutzer frei ändern und sende dann beim Speichern; bei Erfolg refetch oder aktualisiere den Cache.
Entscheide früh, was beim Navigieren weg persistieren soll. Diese eine Entscheidung verhindert viele Überraschungs-Bugs. Inline-Edit-Modi und geöffnete Dropdowns sollten normalerweise zurückgesetzt werden, während ein langer Wizard-Draft oder ein ungesendeter Nachrichten-Entwurf persistieren kann. Überlade nur dann über Reload hinweg, wenn Nutzer es klar erwarten (z. B. Checkout).
Halte Validierungsregeln an einem Ort. Wenn du Regeln über Inputs, Submit-Handler und Helfer verstreust, bekommst du widersprüchliche Fehler. Bevorzuge ein Schema (oder eine validate()-Funktion) und lass die UI entscheiden, wann Fehler gezeigt werden (onChange, onBlur oder onSubmit).
Beispiel: Du generierst einen Edit-Profile-Screen in Koder.ai. Lade das gespeicherte Profil als Serverzustand. Erstelle Draft-State für die Formularfelder. Zeige „ungespeicherte Änderungen“ durch Vergleich Draft vs. gespeichert. Wenn der Nutzer abbricht, verwerfe den Draft und zeige die Server-Version. Wenn er speichert, sende den Draft und ersetze die gespeicherte Version durch die Server-Antwort.
Wenn eine generierte React-App wächst, ist es üblich, dass dieselben Daten an drei Orten landen: Komponentenzustand, globaler Store und Cache. Die Lösung ist meist kein neues Library. Es ist, ein Zuhause pro Zustand zu wählen.
Ein Aufräumfluss, der in den meisten Apps funktioniert:
filteredUsers, wenn du ihn aus users + filter berechnen kannst. Bevorzuge selectedUserId gegenüber einem duplizierten selectedUser Objekt.Beispiel: Eine von Koder.ai generierte CRUD-App beginnt oft mit einem useEffect-Fetch plus einer globalen Store-Kopie derselben Liste. Nachdem du Serverzustand zentralisiert hast, kommt die Liste von einer Query, und „Refresh“ wird zu Invalidation statt zu manuellem Sync.
Für das Naming, halte es konsistent und langweilig:
users.list, users.detail(id)ui.isCreateModalOpen, filters.userSearchopenCreateModal(), setUserSearch(value)users.create, users.update, users.deleteDas Ziel ist: eine Quelle der Wahrheit pro Sache, mit klaren Grenzen zwischen Server- und Clientzustand.
State-Probleme beginnen klein, und eines Tages ändert du ein Feld und drei Teile der UI stimmen nicht mehr überein, welche der „wirkliche“ Wert ist.
Das deutlichste Warnzeichen ist duplizierte Daten: derselbe Nutzer oder Warenkorb lebt in einer Komponente, einem globalen Store und einem Request-Cache. Jede Kopie aktualisiert sich zu einem anderen Zeitpunkt, und du schreibst mehr Code nur um sie gleich zu halten.
Ein weiteres Zeichen ist Sync-Code: Effekte, die Zustand hin und her schieben. Muster wie „wenn Query-Daten sich ändern, update den Store“ und „wenn der Store sich ändert, refetch“ funktionieren oft, bis ein Randfall veraltete Werte oder Schleifen auslöst.
Einige schnelle Warnsignale:
needsRefresh, didInit, isSaving hinzu, die niemand wieder löscht.Beispiel: Du generierst ein Dashboard in Koder.ai und fügst ein Edit-Profile-Modal hinzu. Wenn Profildaten im Query-Cache gespeichert, in einen globalen Store kopiert und im lokalen Form-State dupliziert werden, hast du jetzt drei Wahrheiten. Sobald du Background-Refetching oder optimistische Updates hinzufügst, zeigen sich Unterschiede.
Wenn du diese Zeichen siehst, ist der langweilige Schritt: wähle einen Besitzer pro Datenstück und lösche die Spiegel.
„Nur für den Fall“ zu speichern ist eine der schnellsten Methoden, Zustand schmerzhaft zu machen, besonders in generierten Apps.
Das Kopieren von API-Antworten in einen globalen Store ist eine häufige Falle. Wenn Daten vom Server kommen (Listen, Details, Nutzerprofil), kopiere sie nicht standardmäßig in einen Client-Store. Wähle ein Zuhause für Serverdaten (meistens der Query-Cache). Nutze den Client-Store für UI-only Werte, die der Server nicht kennt.
Abgeleitete Werte zu speichern ist eine weitere Falle. Counts, gefilterte Listen, Summen, canSubmit und isEmpty sollten meist aus Inputs berechnet werden. Wenn Performance wirklich ein Problem wird, memoize, aber speichere nicht von Anfang an das Ergebnis.
Ein einzelner Mega-Store für alles (Auth, Modals, Toasts, Filter, Drafts, Onboarding-Flags) wird schnell zur Ablage. Teile nach Feature-Grenzen. Wenn Zustand nur von einem Screen genutzt wird, behalte ihn lokal.
Context ist großartig für stabile Werte (Theme, aktuelle User-ID, Locale). Für schnell ändernde Werte kann Context breite Re-Renders auslösen. Nutze Context für Verkabelung und Komponentenstate (oder einen kleinen Store) für häufig ändernde UI-Werte.
Zuletzt: vermeide inkonsistente Namensgebung. Fast-duplicate Query-Keys und Store-Felder erzeugen subtile Duplikate. Wähle einen einfachen Standard und halte dich daran.
Wenn du den Drang verspürst, „nur noch eine“ Zustandsvariable hinzuzufügen, mach einen kurzen Ownership-Check.
Erstens: Kannst du eine Stelle benennen, wo Server-Fetching und Caching passieren (ein Query-Tool, ein Set von Query-Keys)? Wenn dieselben Daten in mehreren Komponenten gefetcht werden und zudem in einen Store kopiert sind, zahlst du bereits Zins.
Zweitens: Wird der Wert nur in einem Screen gebraucht (z. B. „ist Filter-Panel offen“)? Dann sollte er nicht global sein.
Drittens: Kannst du eine ID speichern statt ein Objekt zu duplizieren? Speichere selectedUserId und lese den Nutzer aus deinem Cache oder deiner Liste.
Viertens: Ist es abgeleitet? Wenn du es aus vorhandenem Zustand berechnen kannst, speichere es nicht.
Zuletzt: mach einen Ein-Minuten-Trace-Test. Wenn ein Kollege nicht in unter einer Minute beantworten kann „woher kommt dieser Wert?“ (Prop, lokaler State, Server-Cache, URL, Store), behebe die Ownership bevor du mehr Zustand hinzufügst.
Stell dir eine generierte Admin-App vor (zum Beispiel von einem Prompt in Koder.ai) mit drei Screens: Kundenliste, Kunden-Detailseite und Edit-Form.
Zustand bleibt ruhig, wenn er offensichtliche Zuordnungen hat:
Die List- und Detail-Seiten lesen Serverzustand aus dem Query-Cache. Beim Speichern legst du Kunden nicht erneut in einen globalen Store. Du sendest die Mutation und lässt den Cache aktualisieren oder refetchen.
Für den Edit-Screen behalte den Form-Draft lokal. Initialisiere ihn aus dem gefetchten Kunden, aber behandle ihn als separat, sobald der Nutzer zu tippen beginnt. So kann die Detail-Ansicht sicher refetchen, ohne halb fertige Änderungen zu überschreiben.
Optimistisches UI ist ein Bereich, wo Teams oft alles duplizieren. Meistens musst du das nicht.
Wenn der Nutzer Save drückt, aktualisiere nur den gecachten Kunden-Datensatz und den passenden Listeneintrag, und rolle zurück, wenn die Anfrage fehlschlägt. Behalte den Draft im Formular, bis der Save erfolgreich ist. Bei Fehler zeige eine Fehlermeldung und lass den Draft unverändert, damit der Nutzer erneut versuchen kann.
Angenommen, du fügst Bulk Edit hinzu und brauchst ebenfalls ausgewählte Zeilen. Bevor du einen neuen Store erstellst, frag: Soll dieser Zustand Navigation und Reloads überdauern?
Generierte Screens können schnell multiplizieren, und das ist gut — bis jeder neue Screen eigene Zustandsentscheidungen mitbringt.
Schreibe eine kurze Team-Notiz ins Repo: was zählt als Serverzustand, was als Clientzustand und welches Tool besitzt jeweils was. Halte es kurz genug, damit Leute es tatsächlich befolgen.
Füge eine kleine PR-Gewohnheit hinzu: markiere jede neue Zustandsvariable als Server oder Client. Wenn es Serverzustand ist, frag „wo lädt es, wie wird es gecached und was invalidiert es?“ Wenn es Clientzustand ist, frage „wer besitzt es und wann wird es zurückgesetzt?"
Wenn du Koder.ai (koder.ai) nutzt, kann der Planning Mode helfen, State-Grenzen zu vereinbaren, bevor du neue Screens generierst. Snapshots und Rollbacks geben dir einen sicheren Weg, zu experimentieren, wenn eine Zustandsänderung schiefgeht.
Wähle ein Feature (zum Beispiel Edit Profile), wende die Regeln vollständig an und lasse das das Beispiel sein, das alle kopieren.
Beginne damit, jedes Stück Zustand als Server, Client (UI) oder abgeleitet zu kennzeichnen.
isValid).Wenn du sie markiert hast, sorge dafür, dass jedes Element einen offensichtlichen Besitzer hat (Query-Cache, lokale Komponente, URL oder ein kleiner Store).
Verwende diesen kurzen Test: „Kann ich die Seite neu laden und das von Server neu aufbauen?“
Beispiel: Eine Projektliste ist Serverzustand; die ID der ausgewählten Zeile ist Clientzustand.
Weil du dann zwei Wahrheiten hast.
Wenn du users fetcht und sie in useState oder einen globalen Store kopierst, musst du sie während folgender Fälle synchron halten:
Standardregel: und lokale Zustände nur für UI-only Anliegen oder Drafts verwenden.
Speichere abgeleitete Werte nur, wenn du sie wirklich nicht günstig berechnen kannst.
Normalerweise berechnest du sie aus vorhandenen Inputs:
visibleUsers = users.filter(...)total = items.reduce(...)canSubmit = isValid && !isSavingWenn Performance ein echtes Problem wird (gemessen), nutze oder bessere Datenstrukturen, bevor du zusätzlichen gespeicherten Zustand einführst, der leicht veralten kann.
Standard: nutze ein Server-State-Tool (häufig TanStack Query), damit Komponenten einfach nach Daten fragen und Lade-/Fehlerzustände anzeigen können.
Praktische Grundlagen:
Halte es lokal, bis du einen echten Sharing-Bedarf benennen kannst.
Promotionsregel:
So vermeidest du, dass dein globaler Store zum Ablagemechanismus für zufällige UI-Flags wird.
Speichere IDs und kleine Flags, nicht komplette Serverobjekte.
Beispiel:
selectedUserIdselectedUser (kopiertes Objekt)Render die Details, indem du den Nutzer aus dem gecachten List-/Detail-Query nachschlägst. So funktionieren Hintergrund-Refetches und Updates korrekt, ohne zusätzlichen Synchronisationscode.
Behandle das Formular als Draft (Clientzustand), bis du es absendest.
Praktisches Muster:
So vermeidest du, dass du Serverdaten „in-place“ bearbeitest und gegen Refetches kämpfst.
Häufige Warnzeichen:
needsRefresh, didInit, isSaving tauchen auf.Generierte Screens können schnell gemischte Muster hervorbringen. Eine einfache Absicherung ist, Besitz klar zu standardisieren:
Wenn du Koder.ai nutzt, hilft Planning Mode dabei, Besitz vor dem Generieren neuer Screens festzulegen, und Snapshots/Rollbacks geben dir einen sicheren Weg, Experimente zurückzunehmen.
useMemoVermeide verstreute refetch()-Aufrufe „nur um sicherzugehen.“
Die Lösung ist meist nicht eine neue Bibliothek, sondern: Spiegel löschen und für jeden Wert einen Besitzer wählen.