Kodokon kodokon.com

useReducer für komplexen Zustand

Strukturiere komplexen Zustand mit einem reinen Reducer, der Übergänge zentralisiert und unmögliche Kombinationen ausschließt.

9 Min. · 3 Fragen

Diese Lektion in Kodokon öffnen

Sobald eine Komponente mehrere Zustandswerte orchestriert, die sich gemeinsam entwickeln - ein Anfragestatus, ein paar Daten, ein Fehler -, verteilt das Vervielfachen von useState-Aufrufen die Übergangslogik über jeden Event-Handler. Du kennst das Symptom: drei Setter, die in der richtigen Reihenfolge aufgerufen werden müssen, und früher oder später ein inkonsistenter Zustand, etwa ein Status success neben einem Fehler, der nicht null ist. useReducer dreht die Verantwortung um: Der Zustand wird zu einem einzigen Objekt, und jede Änderung läuft über eine Action, die von einer zentralen Funktion, dem Reducer, interpretiert wird.

JAVASCRIPT
function requestReducer(state, action) {
  switch (action.type) {
    case "start":
      return { status: "loading", data: null, error: null };
    case "resolve":
      return {
        status: "success",
        data: action.data,
        error: null,
      };
    case "reject":
      return {
        status: "error",
        data: null,
        error: action.error,
      };
    default:
      throw new Error("Unknown action: " + action.type);
  }
}
Jeder Übergang baut einen vollständigen, konsistenten Zustand neu auf.

Der Reducer ist eine reine Funktion: Er erhält den aktuellen Zustand und eine Action und gibt dann den nächsten Zustand zurück, ohne Seiteneffekte. Die Vorteile zeigen sich sofort: Übergänge sind explizit und vollständig, unmögliche Zustände verschwinden an der Wurzel (jedes case baut ein konsistentes Objekt neu auf), und die Logik liest sich an einer einzigen Stelle, statt verstreut zu sein. Ein nützliches Warnsignal: Wenn du dieselbe Abfolge von setState-Aufrufen an zwei Stellen kopierst, fehlt dir wahrscheinlich ein Reducer.

JSX
function UserProfile() {
  const [state, dispatch] = useReducer(requestReducer, {
    status: "idle",
    data: null,
    error: null,
  });

  async function load(id) {
    dispatch({ type: "start" });
    try {
      const res = await fetch("/api/users/" + id);
      const data = await res.json();
      dispatch({ type: "resolve", data });
    } catch (error) {
      dispatch({ type: "reject", error });
    }
  }

  return <button onClick={() => load(1)}>Load</button>;
}
Die Komponente drückt die Absicht aus, der Reducer entscheidet über den Zustand.

Die Wahl zwischen useState und useReducer ist keine Geschmacksfrage. Behalte useState für einfache, unabhängige Werte; wechsle zu useReducer, wenn der nächste Zustand vom vorherigen abhängt oder wenn sich mehrere Felder gemeinsam ändern. Zwei weitere Vorteile: Das dritte Argument bietet eine verzögerte Initialisierung (useReducer(reducer, arg, init) führt init nur beim ersten Rendern aus), und React garantiert, dass dispatch von einem Rendern zum nächsten dieselbe Identität behält - du kannst es ohne useCallback an Kinder weitergeben.

Wissenscheck

Stelle sicher, dass du die wichtigsten Punkte dieser Lektion behalten hast.

  1. Welches Signal sollte dich eher zu useReducer als zu mehreren useState-Aufrufen führen?
    • Die Komponente überschreitet hundert Zeilen
    • Mehrere Zustandswerte ändern sich gemeinsam und der neue Zustand hängt vom vorherigen ab
    • Die Komponente erhält mehr als drei Props
    • Der Zustand enthält ein Array
  2. Ein Reducer gibt das erhaltene Zustandsobjekt zurück, an Ort und Stelle mutiert. Was macht React?
    • Es vergleicht die alte und die neue Referenz, findet sie gleich und überspringt das erneute Rendern
    • Es erkennt die tiefe Mutation und rendert die Komponente neu
    • Es wirft einen Fehler, weil der Zustand in der Entwicklung eingefroren ist
  3. Warum kannst du dispatch ohne besondere Vorkehrungen an ein memoisiertes Kind weitergeben?
    • Weil React.memo Props vom Typ Funktion ignoriert
    • Weil dispatch bei jedem Rendern neu erzeugt wird, aber strukturell gleich ist
    • Weil React garantiert, dass dispatch für die gesamte Lebensdauer der Komponente dieselbe Identität behält