navigation Navigation


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-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

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

    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.

    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.

    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.

    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.

    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.

    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.

    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.

    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.

    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.

    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.

    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