useReducer ist ein React-Hook, der State wie useState hält, aber jede Art, ihn zu ändern, in eine Funktion verlagert, den Reducer. Deine Komponente ruft dispatch mit einem Action-Objekt wie { type: 'increment' } auf, und React übergibt den aktuellen State und diese Action an den Reducer, der den nächsten State zurückgibt.
Die Buttons sagen nicht, wie sich der Zähler ändert, nur was passiert ist. Die ganze Arithmetik lebt in reducer. Füge ein case 'double' hinzu, das { count: state.count * 2 } zurückgibt, und einen Button, der es dispatcht, um zu sehen, wie eine neue Art von Update hineinpasst.
Die Syntax
const [state, dispatch] = useReducer(reducer, initialArg, init); // init is optional
function reducer(state, action) {
// return the next state
}
reducerist eine Funktion(state, action) => nextState. Definiere sie außerhalb der Komponente; sie braucht nichts aus dem Scope der Komponente.initialArgist der Anfangszustand, der nur beim ersten Render verwendet wird.initist optional. Wenn du es übergibst, ist der Anfangszustand stattdesseninit(initialArg).stateist der aktuelle State für diesen Render.dispatch(action)schickt eine Action an den Reducer und plant einen Render mit dem Ergebnis ein. Es ist stabil: Es ist bei jedem Render dieselbe Funktion, also kannst du es gefahrlos nach unten reichen oder als Abhängigkeit angeben.
Eine Action kann jeder Wert sein, aber nach Konvention ist sie ein Objekt mit einem String type, der beschreibt, was passiert ist, plus allen Daten, die der Reducer braucht: { type: 'added', text: 'Buy milk' }.
Einen Reducer schreiben
Die meisten Reducer sind ein switch über action.type, mit einem case pro Art von Event. Jeder Fall gibt einen brandneuen State zurück; er ändert nie den alten. Der Fall default wirft einen Fehler, also scheitert ein Tippfehler wie dispatch({ type: 'incremnet' }) laut, statt nichts zu tun.
Benenne Actions nach dem, was jemand getan hat (added, toggled, deleted), nicht nach der State-Änderung, die du im Kopf hast (setTodos). Die Komponente liest sich dann wie eine Liste von Events, und der Reducer ist der einzige Ort, der entscheidet, was jedes Event bedeutet.
Reducer müssen pur sein
Ein Reducer ist eine pure Funktion: Bei gleichem State und gleicher Action gibt er dasselbe Ergebnis zurück und tut sonst nichts. Das heißt keine Mutation, keine Anfragen, keine Timer, kein Math.random() oder Date.now() darin. React verlässt sich darauf. In der Entwicklung unter StrictMode ruft React deinen Reducer für jede Action zweimal auf und behält ein Ergebnis, damit dir ein unpurer Reducer auffällt. Die Vorschau hier läuft wie ein Produktions-Build, also rufen die Beispiele auf dieser Seite ihn einmal auf.
Die Regel gegen Mutation wird am häufigsten gebrochen. In state.todos zu pushen und state zurückzugeben gibt dasselbe Objekt zurück, also sieht React keine Änderung und überspringt den Render, dieselbe Falle wie unter Arrays und Objekte aktualisieren beschrieben. Baue neue Arrays und Objekte mit Spread, map und filter.
// Wrong: mutates and returns the same object
case 'added':
state.todos.push(action.todo);
return state;
// Right: returns a new object with a new array
case 'added':
return { ...state, todos: [...state.todos, action.todo] };
Nebeneffekte, die zu einem Event gehören, wie das Speichern auf einem Server, kommen in den Event-Handler neben dispatch. Nebeneffekte, die zum State gehören, wie das Synchronisieren mit localStorage, kommen in einen Effekt.
Eine Todo-Liste mit useReducer
Hier leistet der Reducer echte Arbeit: Todos hinzufügen, abhaken und löschen. Das console.log am Anfang des Reducers gibt jede Action aus, ein praktischer Weg, beim Debuggen zu beobachten, wie sich der State ändert. Es ist der eine Nebeneffekt, der in einem Reducer üblicherweise toleriert wird, weil er nichts ändert; nimm ihn vor dem Ausliefern heraus.
Füge ein Todo hinzu, hake ein Kästchen an und lösch eines, dann lies die Konsole: Jede Änderung ist eine Zeile, die die Action und ihre Daten nennt. Der Text des Inputs bleibt in useState, weil er lokal zum Feld gehört und kein anderes Event ihn berührt. Beide Hooks in einer Komponente zu mischen ist normal.
Die ID des neuen Todos wird im Klick-Handler erzeugt und reist in der Action mit, also kopiert der Reducer sie nur. id: nextId++ im Reducer zu schreiben würde ihn unpur machen: Unter StrictMode in der Entwicklung würde der zweite Aufruf eine ID überspringen.
dispatch ändert den State nicht sofort
dispatch funktioniert wie ein useState-Setter: Es plant einen Render ein, und die Variable state in deinem aktuellen Handler behält den alten Wert. Um den neuen State sofort zu nutzen, berechne ihn selbst, indem du den Reducer aufrufst.
Klicke ein paar Mal. Die erste Log-Zeile ist immer einen Schritt hinterher, und die zweite stimmt nach dem Render mit der Zahl auf dem Button überein. Weil der Reducer eine einfache Funktion ist, kannst du ihn gefahrlos selbst aufrufen, ein weiterer Vorteil davon, ihn pur zu halten.
useState vs. useReducer
Beide Hooks speichern State, und alles, was du mit dem einen schreibst, kannst du mit dem anderen schreiben. Der Unterschied ist, wo die Update-Logik lebt.
| useState | useReducer | |
|---|---|---|
| Update-Logik | In jedem Event-Handler | In einer Reducer-Funktion |
| Gut für | Ein paar unabhängige Werte | Mehrere Felder, die viele Events gemeinsam ändern |
| Codeumfang | Weniger bei einfachem State | Anfangs mehr, weniger, wenn die Events wachsen |
| Testen | Über die Komponente testen | Den Reducer als einfache Funktion testen |
| Debuggen | Herausfinden, welcher Handler ihn gesetzt hat | Jede Action an einer Stelle loggen |
Greif zu useReducer, wenn du bemerkst, dass derselbe State in vielen Handlern aktualisiert wird, wenn ein Event mehrere State-Werte ändern muss, die konsistent bleiben müssen, oder wenn ein Handler vor allem aus Logik über den nächsten State besteht. Für einen Schalter oder ein Textfeld ist useState kürzer und klarer. Später zu wechseln ist nicht schwer: Ersetze die Setter durch Dispatches und verschiebe die Logik in Fälle.
Lazy Initialisierung
Wenn du den Anfangszustand als Funktionsaufruf schreibst, etwa useReducer(reducer, createInitialState('Ada')), läuft dieser Aufruf bei jedem Render, obwohl React sein Ergebnis nur beim ersten Mal nutzt. Wenn der Aufbau des Anfangszustands teuer ist oder aus einer Prop abgeleitet werden soll, übergib ein drittes Argument: eine Funktion init. React ruft init(initialArg) einmal auf, beim ersten Render.
Tippe in das Input: Die Komponente rendert bei jedem Tastendruck, aber createInitialState loggt nur einmal. Ändere jetzt den Aufruf zu useReducer(reducer, createInitialState('Ada')) und tippe erneut. Die Funktion läuft bei jedem Render, und React wirft das Ergebnis nach dem ersten Mal jedes Mal weg.
Übergib die Funktion selbst, nicht das Ergebnis ihres Aufrufs. useReducer(reducer, 'Ada', createInitialState) ist lazy; useReducer(reducer, createInitialState('Ada')) nicht.
useReducer mit Context
Ein Reducer passt gut zu Context, wenn ein tiefer Baum den State braucht. Lege den State und dispatch oben in Context, und jede Komponente darunter kann den State lesen oder Actions senden, ohne dass Props durch jede Ebene gereicht werden. Weil sich dispatch nie ändert, können Komponenten, die nur Actions senden, einen separaten Dispatch-Context lesen und das erneute Rendern auslassen, wenn sich der State ändert.
import { createContext, useContext, useReducer } from 'react';
const TodosContext = createContext(null);
const TodosDispatchContext = createContext(null);
export function TodosProvider({ children }) {
const [todos, dispatch] = useReducer(todosReducer, []);
return (
<TodosContext value={todos}>
<TodosDispatchContext value={dispatch}>{children}</TodosDispatchContext>
</TodosContext>
);
}
function AddTodo() {
const dispatch = useContext(TodosDispatchContext);
return <button onClick={() => dispatch({ type: 'added', text: 'New' })}>Add</button>;
}
In React 19 funktioniert ein Context-Objekt als sein eigener Provider (<TodosContext value={...}>); <TodosContext.Provider> funktioniert weiterhin. Die Seite zu useContext behandelt, wie Context Komponenten erreicht und wann er sie neu rendert.
Häufig gestellte Fragen
Was ist useReducer in React?
Ein Hook für State, dessen Updates als Actions beschrieben werden. Du rufst const [state, dispatch] = useReducer(reducer, initialState) auf und dann aus Event-Handlern dispatch({ type: 'added' }). React übergibt den aktuellen State und die Action an deinen reducer, und was er zurückgibt, wird zum nächsten State.
Wann sollte ich useReducer statt useState nutzen?
Wenn mehrere Events denselben State auf zusammenhängende Weise aktualisieren, wenn der nächste State von mehreren Feldern gleichzeitig abhängt oder wenn die Update-Logik so lang ist, dass du sie in einer testbaren Funktion außerhalb der Komponente haben willst. Für einen oder zwei unabhängige Werte ist useState einfacher.
Warum muss ein Reducer pur sein?
React kann deinen Reducer für dieselbe Action mehr als einmal aufrufen (StrictMode tut das in der Entwicklung absichtlich) und erwartet jedes Mal dasselbe Ergebnis. Ein Reducer darf also den State nicht mutieren, keine Daten laden, keine Timer setzen und keine Zufallswerte lesen. Erledige so etwas in Event-Handlern oder Effekten.
Aktualisiert dispatch den State sofort?
Nein. Wie ein useState-Setter plant dispatch einen Render ein. Im aktuellen Event-Handler enthält state weiterhin den alten Wert; der neue State erscheint im nächsten Render.
Was ist das dritte Argument von useReducer?
Eine optionale Funktion init. Wenn du sie übergibst, berechnet React den Anfangszustand als init(initialArg), und zwar nur beim ersten Render. Das ist nützlich, wenn der Aufbau des Anfangszustands teuer ist oder von einer Prop abhängt.