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.

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.

JavaScript Syntax
const [state, dispatch] = useReducer(reducer, initialState, init);

Parameter

  • reducer: Die Reducer-Funktion
  • initialState: Der initiale State-Wert
  • init: (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

JavaScript Reducer-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.
TypeScript Minimale Struktur
{
    type: "INCREMENT"
}
TypeScript Erweiterte Struktur
{
    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.

JavaScript Viele useState
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.

  1. Ohne useReducer
  2. Mit Reducer-Funktion und mit useReducer

Wir beginnen mit der Version ohne Reducer und schaffen damit unsere Basis und Struktur der Komponente.

TypeScript CounterBasic.jsx
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.

React Hooks - useReducer - Beispiel 1 ohne useReducer

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.

TypeScript reduceCounter.js
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.

TypeScript CounterWithReducer.jsx
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.

TypeScript initialFormState
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.

TypeScript formReducer Funktion
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.

TypeScript FormComponent.jsx
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.

React Hook - useReducer - Beispiel 2

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.

TypeScript Initialer Zustand
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.

TypeScript Reducer-Funktion
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.

TypeScript Filter- und Sortierfunktion
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.

TypeScript TodoComponent.jsx
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.

React Hook - useReducer - Beispiel 3 - Todo Component

Besonderheiten

useReducer vs. useState — die Faustregel.

useState: 1-3 unabhängige Werte, einfache Setter. useReducer: ab ~4 Werten, komplexe State-Übergänge mit klaren Aktionen (LOAD, ADD, REMOVE, RESET), oder wenn nächster State von mehreren vorherigen Werten abhängt.

Reducer MUSS reine Funktion sein — kein Side-Effect, kein Mutate.

Der Reducer bekommt aktuellen State + Action, gibt NEUEN State zurück. KEINE Mutation am alten State (state.x = 5), KEINE Network-Calls, KEINE console.log mit Identity-Output. Reine Berechnung — daher testbar, reproduzierbar.

Action-Type als String oder Konstante — TypeScript bevorzugt Discriminated Unions.

In JS: case 'ADD': Strings. In TS: type Action = { type: 'ADD'; payload: X } | { type: 'REMOVE'; id: number } — Discriminated Union, switch ist exhaustiv prüfbar.

dispatch-Funktion ist STABIL — keine Effect-Dependency nötig.

Wie der Setter aus useState. dispatch ändert sich nie zwischen Renders. Daher: in useEffect-Dependencies weglassbar, Linter beschwert sich nicht.

Default-Case mit throw fängt Tippfehler.

default: throw new Error(Unknown action: $&#123;action.type&#125;) — eine vergessene Behandlung wird sofort sichtbar, nicht still „nichts geändert". In TypeScript löst const _: never = action; dasselbe zur Compile-Zeit.

useReducer + Context = lokaler 'Redux-Store' ohne Redux.

Für komplexen geteilten State innerhalb eines Sub-Trees: useReducer im Eltern, state + dispatch über Context nach unten. Spart Redux-Boilerplate, reicht für mittelgroße Apps.

Lazy Initialization: useReducer(reducer, initialArg, initFn).

Dritter Parameter: eine Funktion, die aus initialArg den initialen State berechnet. Läuft nur beim Mount. Praktisch für teure Initialisierung oder zum Wiederverwenden der Reset-Logik im Reducer.

Spreaden statt mutieren — auch im Reducer-Body.

case 'UPDATE': return {...state, value: action.value}; — immer NEUES Objekt. state.value = action.value; return state ist Mutation und bricht React komplett.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Hooks

Zur Übersicht