navigation Navigation


useState()


Der useState Hook ist eine der grundlegenden Funktionen in React, die es funktionalen Komponenten ermöglicht, lokalen Zustand zu verwalten. Eingeführt mit React 16.8 erlaubt er Entwicklern, reaktive Daten ohne Klassenkomponenten zu speichern und zu aktualisieren. Mit seiner einfachen API bestehend aus einem Wertepaar - dem aktuellen Zustand und einer Funktion zu dessen Aktualisierung - bildet useState das Fundament für interaktive und dynamische Benutzeroberflächen in modernen React-Anwendungen.

Inhaltsverzeichnis

    Einführung

    Was ist State und wozu wird es benötigt?

    State ist der Zustand eine Komponente - Daten, die sich während der Laufzeit ändern können und die Benutzeroberfläche beeinflussen. Man kann sich State als eine Art Variablen vorstellen, welche von React “beobachtet” werden. Wenn sich diese Variablen ändern, aktualisiert React automatisch die Darstellung.

    Warum nicht normale Variablen?

    Normale JavaScript-Variablen lösen kein Re-Rendering aus. React weiß nicht, dass sich etwas geändert hat.

    ❌ Funktioniert NICHT in React

    let counter = 0;
    function handleClick() {
        counter = counter + 1;
    }

    React merkt diese Änderung nicht. Die Komponente wird nicht neu gerendert, also nicht aktualisiert.

    Lösung - useState()

    useState() ist ein Hook (eine spezielle React-Funktion), der es ermöglicht, State in Funktionskomponenten zu verwenden.

    Grundlegende Syntax

    const [stateVariable, setStateFunction] = useState(initialValue);

    Die Funktion useState() gibt ein Array mit zwei Elementen zurück.

    • Element 0: Der aktuelle State-Wert
    • Element 1: Eine Funktion zum Aktualisieren des State

    Einfaches Beispiel

    UseStateExample.jsx
    import { useState } from 'react';
    
    function UseStateExample() {
        const [counter, setCounter] = useState(0);
    
        const handleIncrement = () => {
            setCounter(counter + 1);
        };
    
        const handleDecrement = () => {
            setCounter(counter - 1);
        };
    
        return (
            <>
                Aktueller Wert: {counter}
                <hr />
                <button onClick={handleDecrement}>- 1</button>
                <button onClick={handleIncrement}>+ 1</button>
            </>
        );
    }
    
    export default UseStateExample;

    Was passiert beim Aufruf von setCounter?

    1. React merkt sich die Änderung: setCounter(counter + 1) teilt React mit, dass sich der State geändert hat.
    2. Re-Rendering wird ausgelöst: React render die Komponente neu.
    3. Neuer Wert wird verwendet: Beim nächsten Render hat counter den neuen Wert.
    4. UI wird aktualisiert: Die Änderung wird sichtbar.

    Datentypen

    Die Funktion useState() kann jeden JavaScript-Datentyp verwalten.

    String

    const [name, setName] = useState('');

    Boolean

    const [isVisible, setIsVisible] = useState(false);

    Array

    const [items, setItems] = useState(['Item one', 'Item two']);

    Object

    const [user, setUser] = useState({
        name: 'John Doe',
        age: 30,
        email: 'john@example.com'
    });

    Unveränderlichkeit

    State darf niemals direkt verändert werden. Es müssen immer neue Werte erstellt werden.

    ❌ FALSCH: Direkte Verwendung von State bei Arrays

    // Arrays
    const [items, setItems] = useState(['a', 'b']);
    items.push('c');
    setItems(items);

    Hier erkennt React keine Änderung.

    ✅ RICHTIG: Neue Werte erstellen

    setItems([...items, 'c']);

    Hier wird ein neues Array mit dem Spread-Operator erstellt.


    ❌ FALSCH: Direkte Verwendung von State bei Objekten

    // Objects
    const [user, setUser] = useState({ name: 'John', age: 30 });
    user.age = 31;
    setUser(user);

    Hier wird das Original-Objekt verändert. React erkennt keine Änderung.

    ✅ RICHTIG: Neue Werte erstellen

    setUser({ ...user, age: 31 });

    In diesem Fall wird ein neues Objekt mit dem Spread-Oprator erstellt und ein Wert übergeben, welche sich ändern soll.

    Beispiel

    In den folgenden zwei Beispielen schauen wir uns an, wie sich die unterschiedliche Verwendung von Update-Funktionen verhalten und worauf man achten muss.

    Wir werden in der jeweiligen Event-Handler-Funktion die State-Update-Funktion mehrfach aufrufen.

    ❌ Schlechtes Beispiel

    UseStateExample.jsx
    import { useState } from 'react';
    
    function UseStateExample() {
        const [counter, setCounter] = useState(0);
    
        const handleIncrementBad = () => {
            setCounter(counter + 1);
            setCounter(counter + 1);
        };
    
        return (
            <>
                Aktueller Zähler: {counter}
                <hr />
                <button onClick={handleIncrementBad}>
                    Zähler erhöhen
                </button>
            </>
        );
    }
    
    export default UseStateExample;

    In diesem Beispiel könnte man annehmen, dass der Zähler um 2 erhöht werden sollte. Dabei ist es nicht der Fall. Hier erhöht sich der Zähler tatsächlich lediglich um 1.


    ✅ Gutes Beispiel

    UseStateExample.jsx
    import { useState } from 'react';
    
    function UseStateExample() {
        const [counter, setCounter] = useState(0);
    
        const handleIncrementGood = () => {
            setCounter(prevCounter => prevCounter + 1);
            setCounter(prevCounter => prevCounter + 1);
        };
    
        return (
            <>
                Aktueller Zähler: {counter}
                <hr />
                <button onClick={handleIncrementGood}>
                    Zähler erhöhen
                </button>
            </>
        );
    }
    
    export default UseStateExample;

    In diesem Fall wird der Wert immer um 2 erhöht.


    Erklärung

    Der Zähler wird im ersten Beispiel nur um 1 erhöht, weil React die State-Aktualisierungen zusammenfasst (batching) und die setCounter() Funktion nicht sofort den counter-Wert in der aktuellen Funktionsausführung ändert.

    Asynchronität und Batching von State-Updates

    Wenn man die setCounter Funktion (oder jede andere State-Setter-Funktion) aufruft, weist man React an, eine Zustandsaktualisierung zu planen. Diese Aktualisierung geschieht nicht sofort synchron. Stattdessen sammelt React mehrere State-Updates, die innerhalb desselben Ereignis-Handlers oder synchronen Codeblocks auftreten und führt sie in einer einzigen “Batch”-Aktualisierung zusammen aus. Das geschieht aus Performance-Gründen.

    Rolle von Closures

    In der handleIncrementBad() Funktion aus dem schlechten Beispiel passiert Folgendes:

    1. Erster Aufruf von setCounter(counter + 1):
      • Zu diesem Zeitpunkt liest die Funktion den aktuellen Wert von counter aus dem Scope der UseStateExample Komponente. Initial hat counter den Wert 0.
      • setCounter(0 + 1) wird aufgerufen. React plant nun eine Aktualisierung, um den counter auf 1 zu setzen. Wichtig ist hier, dass der Wert von counter in der JavaScript-Variable innerhalb von handleIncrementBad immer noch 0 ist.
    2. Zweiter Aufruf von setCounter(counter + 1):
      • Auch dieser Aufruf liest den Wert von counter aus dem Scope der UseStateExample Funktion. Da die setCounter() Funktion asynchron ist und die Komponente noch nicht neu gerendert wurde, ist der Wert von counter in diesem Scope immer noch 0. JavaScript “erinnert” sich an den Wert von counter zum Zeitpunkt der Erstellung des handleIncrementBad Funktions-Scopes.
      • setCounter(counter + 1) wird erneut aufgerufen. React plant nun eine weitere Aktualisierung, um den counter auf 1 zu setzen.

    Verarbeitung der Batch-Aktualisierung

    Nachdem die handleIncrementBad() Funktion vollständig ausgeführt wurde, verarbeitet React die geplanten State-Updates. Es sieht zwei Anweisungen, den counter auf 1 zu setzen. Da beide auf demselben ursprünglichen Wert basieren, ist das Ergebnis der Batch-Verarbeitung, dass der counter tatsächlich einmal auf 1 gesetzt wird. Die zweite Anweisung überschreibt die erste nicht wirklich mit einem anderen Wert, sondern bestätigt denselben neuen Wert.

    React rendert die Komponente dann neu. In diesem neuen Render-Zyklus hat useState den aktualisierten Wert von counter (also 1) und das wird in der UI angezeigt.

    Warum nicht 2?

    Der entscheidende Punkt ist, dass beide setCounter(counter + 1) Aufrufe mit demselben veralteten counter Wert arbeiten, nämlich dem Wert, den counter zu Beginn der handleIncrementBad() Funktion hatte. Sie bauen nicht aufeinander auf, weil die Variable counter innerhalb dieses Funktionsaufrufs nicht aktualisiert wird.

    Praktisches Beispiel

    In diesem Beispiel bauen wir eine kleine und einfache Todo-App auf. Hier werden wir nochmals sehen, wie man useState() verwendet.

    UseStateExample.jsx
    import { useState } from 'react';
    
    function UseStateExample() {
    
        // State für das Eingabefeld
        const [fieldTodo, setFieldTodo] = useState('');
    
        // State für Aufgaben
        const [todos, setTodos] = useState([
            { id: 1, text: 'Aufgabe eins', completed: false },
            { id: 2, text: 'Aufgabe zwei', completed: true }
        ]);
    
        // State für die Aufgaben-ID
        const [nextId, setNextId] = useState(3);
    
        const addTodo = () => {
            if (fieldTodo.trim() === '') return;
    
            // Neue Aufgabe erstellen
            const newTodo = {
                id: nextId,
                text: fieldTodo.trim(),
                completed: false
            };
    
            // Funktionsbasierte Updates für alle States
            setTodos(prevTodos => [...prevTodos, newTodo]);
            setNextId(prevId => prevId + 1);
            setFieldTodo('');
        };
    
        const toggleTodo = (id) => {
            setTodos(prevTodos => {
                return prevTodos.map(todo => {
                    if (todo.id === id) {
                        return { ...todo, completed: !todo.completed };
                    } else {
                        return todo;
                    }
                });
            });
        };
    
        const deleteTodo = (id) => {
            setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
        };
    
        const handleKeyPress = (e) => {
            if (e.key === 'Enter') addTodo();
        };
    
        const completedCount = todos.filter(todo => todo.completed).length;
        const totalCount = todos.length;
    
        return (
            <div className="todo-app">
                <h2>Todo-App</h2>
                <hr />
                <div className="input-area">
                    <input
                        type="text"
                        value={fieldTodo}
                        onChange={(e) => setFieldTodo(e.target.value)}
                        onKeyDown={handleKeyPress}
                        placeholder="Neue Aufgabe ..."
                    />
                    <button onClick={addTodo} disabled={fieldTodo.trim() === ''}>
                        Hinzufügen
                    </button>
                </div>
                <hr />
                <div className="statistic-area">
                    <p>{completedCount} von {totalCount} Aufgaben erledigt.</p>
                </div>
                <div className="todo-list">
                    {todos.map(todo => (
                        <div key={todo.id}>
                            <input
                                type="checkbox"
                                checked={todo.completed}
                                onChange={() => toggleTodo(todo.id)}
                            />
                            <span>
                                {todo.text}
                            </span>
                            <button onClick={() => deleteTodo(todo.id)}>
                                Löschen
                            </button>
                        </div>
                    ))}
                </div>
            </div>
        );
    
    }
    
    export default UseStateExample;