Hirefullstack – Software Engineering & IT-Beratung aus Berlin
← Zur Übersicht
Kern 12 Min Lesezeit

useReducer

Wenn aus „ein paar Feldern“ eine kleine Maschine geworden ist

Baut auf: useState
In einem Satz

Statt an zehn Stellen setState aufzurufen, schreibst du an einer Stelle auf, wie sich der Zustand bei jedem Ereignis ändert. Die Komponente meldet nur noch, was passiert ist.

useReducer ist der große Bruder von useState. Statt „setze diesen Wert auf das“ sagst du „Folgendes ist passiert“ – und eine einzige Funktion entscheidet, was daraus wird.

Bildlich gesprochen

useState ist ein Schalter, den du selbst umlegst. useReducer ist eine Bestellung: Du rufst „einmal Pizza“ – das Rezept entscheidet, was in der Küche passiert. Und alle Rezepte stehen an einem Ort.

aufbau.ts
const [state, dispatch] = useReducer(reducer, startwert);
//     ▲       ▲                        ▲
//     |       |                        deine Funktion: (alt, ereignis) => neu
//     |       "so ist es passiert" melden
//     der aktuelle Zustand

dispatch({ type: "hinzugefuegt", text: "Milch" });
„Reducer“ ist nur der Name für die Funktion, die aus altem Zustand plus Ereignis den neuen Zustand macht.

Ein vollständiges Beispiel

todos.tsx
type Todo = { id: number; text: string; done: boolean };

// Was alles passieren kann
type Action =
  | { type: "hinzugefuegt"; text: string }
  | { type: "umgeschaltet"; id: number }
  | { type: "geleert" };

// Was daraus wird – eine ganz normale Funktion
function reducer(todos: Todo[], action: Action): Todo[] {
  switch (action.type) {
    case "hinzugefuegt":
      return [...todos, { id: Date.now(), text: action.text, done: false }];

    case "umgeschaltet":
      return todos.map((t) => (t.id === action.id ? { ...t, done: !t.done } : t));

    case "geleert":
      return [];

    default:
      return todos;
  }
}

function App() {
  const [todos, dispatch] = useReducer(reducer, []);

  return <button onClick={() => dispatch({ type: "geleert" })}>Alle löschen</button>;
}
Tipp Ereignisse benennen, keine Setter

Nimm { type: "formular_abgeschickt" } statt { type: "setzeLaden" }. Ein Ereignis kann mehrere Felder auf einmal richtig setzen (laedt: true, fehler: null) – ein Setter kennt nur eines und lässt Lücken.

Der Reducer muss brav bleiben

verändert das Alte, wirkt nach außen
function reducer(state, action) {
  state.count += 1;                // verändert das Alte!
  localStorage.setItem("c", "1");  // wirkt nach außen!
  return state;                    // dasselbe Objekt -> kein Neuzeichnen
}
rechnet nur und gibt Neues zurück
function reducer(state, action) {
  return { ...state, count: state.count + 1 };
}

// Nach außen wirken darf der Klick-Handler:
function klick() {
  dispatch({ type: "erhoeht" });
  localStorage.setItem("c", String(state.count + 1));
}

Der Reducer soll rein (pure)Eine Funktion ist „rein“, wenn sie bei gleicher Eingabe immer dasselbe zurückgibt und sonst nichts anfasst.Deine Komponenten sollen beim Rendern rein sein – dann darf React sie gefahrlos mehrfach aufrufen. sein: nur aus den Eingaben rechnen, ein neues Ergebnis zurückgeben, sonst nichts anfassen. React ruft ihn in der Entwicklung absichtlich doppelt auf – bei einer braven Funktion fällt das nicht auf.

useState oder useReducer?

Nimm es, wenn …
  • +

    Mehrere Felder ändern sich gemeinsam (laedt, daten, fehler)

  • +

    Die nächste Änderung hängt kompliziert vom bisherigen Zustand ab

  • +

    Dieselbe Logik wird von vielen Stellen ausgelöst (Formular-Assistent, Spiel, Rückgängig-Funktion)

  • +

    Du willst die Logik testen können, ohne etwas zu zeichnen

Lass es, wenn …
  • Ein einzelner Schalter oder Zähler – da ist useState kürzer und klarer

  • Wenn der Reducer nur state.feld = action.wert durchreicht: dann ist es useState mit Umweg

Gut zu wissen Praktisch: dispatch bleibt immer dasselbe

Anders als selbstgeschriebene Funktionen ändert sich dispatch nie. Du kannst es also bedenkenlos tief nach unten weitergeben oder in Effect-Listen schreiben.

Genauer erklärt: Startwert berechnen und der never-Trick optional

useReducer nimmt einen dritten Parameter: eine Funktion, die den Startwert erst beim ersten Mal berechnet. Praktisch, wenn das Aufbauen teuer ist.

lazy-init.tsx
function init(anzahl: number) {
  return { felder: Array(anzahl).fill(""), fehler: null };
}

const [state, dispatch] = useReducer(reducer, 5, init);
//                                             ▲   ▲
//                                             |   wird einmal mit 5 aufgerufen
//                                             Argument für init

Und ein hilfreicher TypeScript-Trick: Wenn du im default-Zweig eine Variable vom Typ never deklarierst, meckert der Compiler, sobald du eine Action vergisst.

never.ts
default: {
  const _pruefe: never = action;   // Fehler, falls ein Fall fehlt
  return state;
}

Sitzt das schon?

4 Fragen zu dieser Lektion. Falsche Antworten landen in deiner Statistik.

Jetzt selbst schreiben

Warenkorb als Reducer

Vervollständige den Reducer. Der Warenkorb soll Artikel hinzufügen (ein schon vorhandener erhöht nur die Menge), Mengen verringern und komplett geleert werden können.

  • hinzufuegen legt einen neuen Artikel mit Menge 1 an
  • Nochmal derselbe Artikel erhöht die Menge auf 2
  • verringern senkt die Menge; bei 0 fliegt der Artikel raus
  • leeren entfernt alles
import { useReducer } from "react";

type Zeile = { name: string; menge: number };
type Action =
  | { type: "hinzufuegen"; name: string }
  | { type: "verringern"; name: string }
  | { type: "leeren" };

function reducer(state: Zeile[], action: Action): Zeile[] {
  switch (action.type) {
    case "hinzufuegen":
      // TODO
      return state;
    case "verringern":
      // TODO
      return state;
    case "leeren":
      // TODO
      return state;
    default:
      return state;
  }
}

export default function App() {
  const [korb, dispatch] = useReducer(reducer, []);

  return (
    <div style={{ fontFamily: "system-ui", padding: 16 }}>
      <button onClick={() => dispatch({ type: "hinzufuegen", name: "Apfel" })}>
        Apfel +
      </button>{" "}
      <button onClick={() => dispatch({ type: "verringern", name: "Apfel" })}>
        Apfel −
      </button>{" "}
      <button onClick={() => dispatch({ type: "leeren" })}>Leeren</button>

      <ul>
        {korb.map((z) => (
          <li key={z.name} data-testid={"zeile-" + z.name}>
            {z.name}: {z.menge}
          </li>
        ))}
      </ul>
    </div>
  );
}

Hirefullstack

Ihr braucht React-Verstärkung im Team?

Wir bauen seit Jahren React- und Next.js-Anwendungen für Kunden in ganz Deutschland – als einzelner Experte, als Verstärkung fürs Bestandsteam oder als komplettes Scrum-Team.

Projekt besprechen →