useReducer
Wenn aus „ein paar Feldern“ eine kleine Maschine geworden ist
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.
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.
const [state, dispatch] = useReducer(reducer, startwert);
// ▲ ▲ ▲
// | | deine Funktion: (alt, ereignis) => neu
// | "so ist es passiert" melden
// der aktuelle Zustand
dispatch({ type: "hinzugefuegt", text: "Milch" }); Ein vollständiges Beispiel
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>;
} 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
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
} 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?
- +
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
- −
Ein einzelner Schalter oder Zähler – da ist
useStatekürzer und klarer - −
Wenn der Reducer nur
state.feld = action.wertdurchreicht: dann ist esuseStatemit Umweg
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.
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.
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
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.
- □
hinzufuegenlegt einen neuen Artikel mit Menge 1 an - □Nochmal derselbe Artikel erhöht die Menge auf 2
- □
verringernsenkt die Menge; bei 0 fliegt der Artikel raus - □
leerenentfernt 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> ); }