useReducer()
Der useReducer
Hook ist eine leistungsstarke Alternative zu useState
für die Verwaltung komplexer Zustandslogik in React-Komponenten. Basierend auf dem Reducer-Pattern, das auch in Redux Verwendung findet, ermöglicht dieser Hook die Zentralisierung von Zustandsänderungen durch vordefinierte Aktionen. Statt mehrere voneinander abhängige useState
-Aufrufe zu verwenden, bietet useReducer
einen strukturierten Ansatz für komplexe Statusübergänge, macht den Datenfluss nachvollziehbarer und verbessert die Wartbarkeit. Besonders bei komponenten mit umfangreicher Geschäftslogik oder verschachtelten Zustandsstrukturen zeigt sich die Stärke dieses Hooks, indem er die Zustandslogik von der Darstellungslogik trennt und Zustandsänderungen vorhersehbarer macht.
Inhaltsverzeichnis
Einführung
Der useReducer
ist ein Hook in React, der es ermöglicht, komplexen State (Zustand) in funktionalen Komponenten zu verwalten. Er wurde mit React 16.8 eingeführt und basiert auf dem Reducer-Pattern, das vielen aus Redux bekannt ist.
useReducer
ist eine Alternative zu useState
für das State-Management.
Was ist ein Reducer?
Ein Reducer ist eine reine Funktion, die zwei Parameter entgegennimmt:
- Den aktuellen Zustand (State)
- Eine Aktino (Action)
Diese Funktion gibt basierend auf der Aktion einen neuen Zustand zurück. Der Name “Reducer” kommt daher, dass diese Funktion mehrere mögliche Aktionen auf einen einzigen neuen Zustand “reduziert”.
Grundprinzip
Der Reducer nimmt niemals direkte Änderungen am bestehenden Zustand vor, sondern erstellt immer einen neuen State-Objekt. Dies folgt dem Prinzip der Unveränderlichkeit.
Syntax
Die grundlegende (schematische) Verwendungs-Syntax sieht wie folgt aus.
const [state, dispatch] = useReducer(reducer, initialState, init);
Parameter
reducer
: Die Reducer-FunktioninitialState
: Der initiale State-Wertinit
: (Optional) Eine Funktion zur verzögerten Initialisierung des States
Rückgabewert
Die Funktion useReducer
gibt ein Array mit zwei Elementen zurück.
state
: Der aktuelle Zustand (State)dispatch
: Eine Funktion zum Auslösen von Aktionen (Actions)
Reducer-Funktion Struktur
function reducer(state, action) {
switch (action.type) {
case 'ACTION_TYPE_1':
return { ...state, /* Änderungen */}
case 'ACTION_TYPE_2':
return { ...state, /* Änderungen */}
default:
return state;
}
}
Wichtige Merke
Die Reducer-Funktion muss immer am Ende einen Zustand (State) zurückgeben. Entweder aktualisiert (je nach Aktion) oder im gleichen Zustand.
Aktion-Struktur
Die Aktion ist typischerweise ein Objekt mit mindestens einer Eigenschaft.
type
(string) - Beschreibt die Art der Aktion, also was passieren soll.- Optional weitere Felder, um Details oder Payload mitzugeben.
{
type: "INCREMENT"
}
{
type: "SET_VALUE",
payload: 42
}
Problem
Welches Problem löst nun useReducer()
? Was spricht dagegeben einfach useState()
zu verwenden?
Wenn man mehrere zusammenhängende State-Variablen hat, kann useState()
schnell unübersichtlich werden.
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [isSubmitted, setIsSubmitted] = useState(false);
Wenn State-Updates von mehreren Bedingungen oder vom vorherigen State abhängen, wird die Logik mit useState()
komplex und fehleranfällig.
Verwendungsmöglichkeiten
Wann macht es Sinn useReducer()
zu verwenden?
- Komplexer State: Wenn der State aus mehreren Unterwerten besteht.
- Abhängige Updates: Wenn der nächste State vom vorherigen abhängt.
- Ähnlich wie Redux: Wenn man Redux-ähnliche Patterns in einer Komponente benötigt.
- Performance: Bei tief verschachtelten Komponenten-Bäumen.
- Gemeinsame State-Logik: Wenn mehrere Komponenten ähnliche State-Logik haben.
Beispiel 1 - Zähler Component
Im ersten Beispiel werden zwei Versionen aufbauen.
- Ohne
useReducer
- Mit Reducer-Funktion und mit
useReducer
Wir beginnen mit der Version ohne Reducer und schaffen damit unsere Basis und Struktur der Komponente.
import { useState } from 'react';
function CounterBasic() {
const [counter, setCounter] = useState(0);
const handleIncrement = () => {
setCounter(counter => counter + 1);
};
const handleDecrement = () => {
setCounter(counter => counter - 1);
};
const handleReset = () => {
setCounter(0);
};
return (
<div>
<p>Zähler: {counter}</p>
<div style={{
display: 'flex',
gap: 10,
alignItems: 'center'
}}>
<button onClick={handleIncrement}>
Erhöhen +1
</button>
<button onClick={handleDecrement}>
Reduzieren -1
</button>
<button onClick={handleReset}>
Zurücksetzen
</button>
</div>
</div>
);
}
export default CounterBasic;
Als Resultat haben wir eine Mini-Anwendung, die einen Zähler zur Verfügung stellt.
Im nächsten Part bauen wir die Version des Beispiels mit der Verwendung von useReducer
. Ebenfalls werden wir eine Reducer-Funktion benötigen.
Wir starten mit der Reducer-Funktion. Sprich der Funktion, die für die Aktualisierung der Werte in Abhängigkeit vom Typ der Aktion zuständig ist.
function reduceCounter(state, action) {
switch (action.type) {
case 'INCREMENT':
return { counter: state.counter + 1 };
case 'DECREMENT':
return { counter: state.counter - 1 };
case 'RESET':
return { counter: 0 };
default:
throw new Error(`Unbekannte Aktion: ${action.type}`);
}
}
export default reduceCounter;
Nun bauen wir unsere Komponente auf.
import { useReducer } from 'react';
import reduceCounter from './reduceCounter';
function CounterWithReducer() {
const [state, dispatch] = useReducer(reduceCounter, { counter: 0 });
const handleIncrement = () => {
dispatch({ type: 'INCREMENT' });
};
const handleDecrement = () => {
dispatch({ type: 'DECREMENT' });
};
const handleReset = () => {
dispatch({ type: 'RESET' });
};
return (
<div>
<p>Zähler: {state.counter}</p>
<div style={{
display: 'flex',
gap: 10,
alignItems: 'center'
}}>
<button onClick={handleIncrement}>
Erhöhen +1
</button>
<button onClick={handleDecrement}>
Reduzieren -1
</button>
<button onClick={handleReset}>
Zurücksetzen
</button>
</div>
</div>
);
};
export default CounterWithReducer;
Nun haben wir von der Funktion her eine identische App. Dabei verwenden wir useReducer
und die Reducer-Funktion.
Beispiel 2 - Formular Component
Im zweiten Beispiel bauen wir ein Formular-Component auf, welches den useReducer
Hook verwendet und verschiedene Operationen darin speichert.
Zuerst definieren wir unseren initial Formular-Zustand.
const initialFormState = {
values: {
fieldUsername: '',
fieldEmail: '',
fieldPassword: ''
},
errors: {
fieldUsername: '',
fieldEmail: '',
fieldPassword: ''
},
isSubmitting: false,
isSubmitted: false
};
Es soll darauf geachtet werden, dass dieses Objekt nicht durch ein Scope unerreichbar gemacht wird. Am besten definiert man es auf der Ebene der Funktionen.
Im nächsten Schritt definieren wir die Reducer-Funktion mit allen Handlern für unterschiedliche Aktionstypen.
function formReducer(state, action) {
switch (action.type) {
case 'UPDATE_FIELD':
return {
...state,
values: {
...state.values,
[action.field]: action.value
},
errors: {
...state.errors,
[action.field]: ''
}
}
case 'VALIDATE_FIELD':
const { field, value } = action;
let error = '';
// Feld Validierung
switch (field) {
case 'fieldUsername':
if (value.length < 3) {
error = 'Benutzername muss mindestens 3 Zeichen lang sein';
}
break;
case 'fieldEmail':
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
error = 'Bitte geben Sie eine gültige E-Mail-Adresse ein';
}
break;
case 'fieldPassword':
if (value.length < 6) {
error = 'Passwort muss mindestens 6 Zeichen lang sein';
}
break;
}
return {
...state,
errors: {
...state.errors,
[field]: error
}
};
case 'SUBMIT_START':
return {
...state,
isSubmitting: true
};
case 'SUBMIT_SUCCESS':
return {
...state,
isSubmitting: false,
isSubmitted: true,
values: initialFormState.values,
errors: initialFormState.errors
};
case 'SUBMIT_ERROR':
return {
...state,
isSubmitting: false,
errors: {
...state.errors,
general: action.error
}
};
case 'RESET_FORM':
return initialFormState;
default:
return state;
}
}
Damit haben wir unsere Reducer-Funktion definiert und können nun diese, zusammen mit dem useReducer
Hook in unserem Formular-Component verwenden.
function FormComponent() {
const [state, dispatch] = useReducer(formReducer, initialFormState);
const handleChange = e => {
const { name, value } = e.target;
dispatch({
type: 'UPDATE_FIELD',
field: name,
value
});
};
const handleBlur = e => {
const { name, value } = e.target;
dispatch({
type: 'VALIDATE_FIELD',
field: name,
value
});
};
const handleSubmit = async (e) => {
e.preventDefault();
// Alle Felder validieren
Object.keys(state.values).forEach(field => {
dispatch({
type: 'VALIDATE_FIELD',
field,
value: state.values[field]
})
});
// Prüfen, ob Fehler vorhanden sind
const hasErrors = Object.values(state.errors).some(error => error !== '');
if (hasErrors) return;
dispatch({ type: 'SUBMIT_START' });
try {
// Simuliere API-Request
await new Promise(resolve => setTimeout(resolve => 2000));
console.log('Erfolgreich abgesendet');
console.log(JSON.stringify(state.values));
dispatch({ type: 'SUBMIT_SUCCESS' });
} catch (error) {
dispatch({
type: 'SUBMIT_ERROR',
error: 'Registrierung fehlgeschlagen'
});
}
};
if (state.isSubmitted) {
return <div>✅ Registrierung erfolgreich!</div>;
}
return (
<form onSubmit={handleSubmit}>
<div>
<input
type="text"
name="fieldUsername"
placeholder="Benutzername"
value={state.values.fieldUsername}
onChange={handleChange}
onBlur={handleBlur}
/>
{state.errors.fieldUsername && (
<span style={{ color: 'red' }}>
{state.errors.fieldUsername}
</span>
)}
</div>
<div>
<input
type="text"
name="fieldEmail"
placeholder="E-mail"
value={state.values.fieldEmail}
onChange={handleChange}
onBlur={handleBlur}
/>
{state.errors.fieldEmail && (
<span style={{ color: 'red' }}>
{state.errors.fieldEmail}
</span>
)}
</div>
<div>
<input
type="password"
name="fieldPassword"
placeholder="Passwort"
value={state.values.fieldPassword}
onChange={handleChange}
onBlur={handleBlur}
/>
{state.errors.fieldPassword && (
<span style={{ color: 'red' }}>
{state.errors.fieldPassword}
</span>
)}
</div>
<button type="submit" disabled={state.isSubmitting}>
{state.isSubmitting ? 'Wird gesendet ...' : 'Registrieren'}
</button>
<button
type="button"
onClick={() => dispatch({ type: 'RESET_FORM' })}
>
Zurücksetzen
</button>
</form>
);
}
Damit haben wir ein Formular-Component, bei dem der Zustand vollständig durch useReducer
Hook verwaltet wird.
Damit haben wir hier folgende Punkte:
- Der State enthält alle für das Formular relevante Daten
- Validierung ist zentral im Reducer implementiert
- Verschiedene Actions sind für verschiedene Ereignisse definiert
- Klare Trennung von UI und Geschäftslogik
- Einfach erweiterbar um neue Felder oder Validierungsregeln
Beispiel 3 - Todo Component
Im nächsten Beispiel werden wir ein Todo-Component aufbauen, welches useReducer
verwendet, um komplexere Zustände zu verwalten.
Wir starten mit der Definition unseren initialen State-Objekts. Gleichzeitig ist es auch unsere State-Struktur.
const initialTodoState = {
todos: [],
filter: 'ALL',
sortBy: 'DATE',
searchTerm: ''
};
Folgende Elemente sind vorhanden:
todos
: Ein leeres Array, in welchem die künftig erstellen Aufgaben gesammelt werden.filter
: Kann folgende Werte haben:ALL
|ACTIVE
|COMPLETED
.sortBy
: Kann folgende Werte haben:DATE
|NAME
|PRIORITY
.searchTerm
: Wert, welcher über das Such-Eingabefeld gesetzt wird.
Nun definieren wir die Reducer-Funktion, die alle Aktionen beinhaltet und entsprechend, je nach Aktion, immer einen aktualisierten Zustand zurückgibt.
Wir definieren diese Funktion außerhalb unserer Todo-Komponente. Man kann diese Funktion auch in eine Extra-Datei auslagern, wenn die Struktur dies erfordert.
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [
...states.todos,
{
id: Date.now(),
text: action.text,
completed: false,
priority: action.priority || 'Mittel',
createdAt: new Date().toISOString()
}
]
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo => {
if (todo.id === action.id) {
return { ...todo, completed: !todo.completed }
} else {
return todo;
}
})
};
case 'EDIT_TODO':
return {
...state,
todos: state.todos.map(todo => {
if (todo.id === action.id) {
return { ...todo, text: action.text }
}
return todo;
})
};
case 'SET_FILTER':
return {
...state,
filter: action.filter
};
case 'SET_SORT':
return {
...state,
sortBy: action.sortBy
};
case 'SET_SEARCH':
return {
...state,
searchTerm: action.searchTerm
};
case 'CLEAR_COMPLETED':
return {
...state,
todos: state.todos.filter(todo => !todo.completed)
};
default:
return state;
}
}
Damit haben wir unsere Reducer-Funktion definiert, die alle wichtigen Fälle (Aktionen) abdeckt.
Für unsere Filter- und Sortier Funktionalität erstellen wir ebenfalls eine Extra-Funktion, obwohl man alles innerhalb unserer Component-Funktion implementieren könnte. Durch die Extra-Funktion wird es etwas übersichtlicher.
function getFilteredAndSortedTodos(state) {
let filteredTodos = state.todos;
// Filtern nach Status
switch (state.filter) {
case 'ACTIVE':
filteredTodos = filteredTodos.filter(todo => !todo.completed);
break;
case 'COMPLETED':
filteredTodos = filteredTodos.filter(todo => todo.completed);
break;
}
// Filtern nach Suchbegriff
if (state.searchTerm) {
filteredTodos = filteredTodos.filter(todo => {
return todo.text.toLowerCase().includes(state.searchTerm.toLowerCase());
});
}
// Sortieren
const sortedTodos = [...filteredTodos];
switch (state.sortBy) {
case 'NAME':
sortedTodos.sort((a, b) => a.text.localeCompare(b.text));
break;
case 'PRIORITY':
const priorityOrder = { high: 0, medium: 1, low: 3 };
sortedTodos.sort((a, b) => {
priorityOrder[a.priority] - priorityOrder[b.priority]
});
break;
case 'DATE':
sortedTodos.sort((a, b) => {
new Date(b.createdAt) - new Date(a.createdAt);
});
break;
}
return sortedTodos;
}
Damit haben wir auch die Hilfsfunktion für die Sortierung und Filterung definiert.
Im letzten Schritt bauen wir nun unsere Todo-Komponente auf und verwenden alles, was wir zuvor definiert haben, um die Funktion vollständig zu realisieren.
import { useState, useReducer } from 'react';
function TodoComponent() {
const [state, dispatch] = useReducer(todoReducer, initialTodoState);
const [inputValue, setInputValue] = useState('');
const [priority, setPriority] = useState('Mittel');
const filteredAndSortedTodos = getFilteredAndSortedTodos(state);
const handleAddTodo = e => {
e.preventDefault();
if (inputValue.trim()) {
dispatch({
type: 'ADD_TODO',
text: inputValue,
priority
});
setInputValue('');
}
};
const stats = {
total: state.todos.length,
active: state.todos.filter(t => !t.completed).length,
completed: state.todos.filter(t => t.completed).length
};
return (
<div className="todo-component">
<h2>Todo Liste</h2>
<hr />
<form onSubmit={handleAddTodo}>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Neue Aufgabe ..."
/>
<select
value={priority}
onChange={(e) => setPriority(e.target.value)}
>
<option value="low">Niedrig</option>
<option vlaue="medium">Mittel</option>
<option value="high">Hoch</option>
</select>
<button type="submit">
Hinzufügen
</button>
</form>
<hr />
<div className="search-and-filter">
<input
type="text"
value={state.searchTerm}
placeholder="Suche ..."
onChange={(e) => dispatch({
type: 'SET_SEARCH',
searchTerm: e.target.value
})}
/>
<div className="filter">
Filter:
<button
style={{ fontWeight: state.filter === 'ALL' ? 'bold' : 'normal' }}
onClick={() => dispatch({ type: 'SET_FILTER', filter: 'ALL' })}
>
Alle ({stats.total})
</button>
<button
style={{ fontWeight: state.filter === 'ACTIVE' ? 'bold' : 'normal' }}
onClick={() => dispatch({ type: 'SET_FILTER', filter: 'ACTIVE' })}
>
Aktiv ({stats.active})
</button>
<button
style={{ fontWeight: state.filter === 'COMPLETED' ? 'bold' : 'normal' }}
onClick={() => dispatch({ type: 'SET_FILTER', filter: 'COMPLETED' })}
>
Erledigt ({stats.completed})
</button>
</div>
<div className="sort">
Sortieren:
<select
value={state.sortBy}
onChange={(e) => dispatch({
type: 'SET_SORT',
sortBy: e.target.value
})}
>
<option value="DATE">Datum</option>
<option value="NAME">Name</option>
<option value="PRIORITY">Priorität</option>
</select>
</div>
</div>
<ul className="todo-list">
{filteredAndSortedTodos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch({
type: 'TOGGLE_TODO',
id: todo.id
})}
/>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none',
color: todo.priority === 'high' ? 'red' : todo.priority === 'low' ? 'gray' : 'black'
}}>
{todo.text}
</span>
<span> [{todo.priority}]</span>
<button
onClick={() => dispatch({
type: 'DELETE_TODO',
id: todo.id
})}
>
Löschen
</button>
</li>
))}
</ul>
<hr />
{stats.completed > 0 && (
<button onClick={() => dispatch({ type: 'CLEAR_COMPLETED' })}>
Erledigte löschen ({stats.completed})
</button>
)}
</div>
);
}
export default TodoComponent;
Als Resultat haben wir eine kleine Anwendung, bestehend aus einem Component aufgebaut, in der wir unsere Aufgaben mithilfe von useReducer
verwalten können.