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.

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

JavaScript OhneState.jsx
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

JavaScript 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

TypeScript 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

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

Boolean

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

Array

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

Object

JavaScript 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

JavaScript Array mutiert (falsch)
const [items, setItems] = useState(['a', 'b']);
items.push('c');
setItems(items);

Hier erkennt React keine Änderung.

✅ RICHTIG: Neue Werte erstellen

JavaScript Array immutable (richtig)
setItems([...items, 'c']);

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

❌ FALSCH: Direkte Verwendung von State bei Objekten

JavaScript Object mutiert (falsch)
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

JavaScript Object immutable (richtig)
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

TypeScript 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

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

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

Häufige Stolperfallen

setState ist ASYNCHRON — neuer Wert kommt erst im nächsten Render.

Nach setCount(count + 1) hat count in derselben Funktion noch den alten Wert. Wer mehrere Updates verkettet, sollte die funktionale Form nutzen: setCount(c => c + 1).

Object/Array-State: spreaden, nicht mutieren.

state.foo = 5 ändert die Referenz nicht. React rendert nicht neu. Korrekt: setState(prev => ({...prev, foo: 5})). Bei Arrays: setArr(prev => [...prev, neu]), niemals push.

Funktionaler Setter ist Pflicht bei mehreren Updates pro Event.

setCount(count + 1); setCount(count + 1); ergibt +1, nicht +2 — beide Aufrufe sehen denselben alten count. Mit funktionalem Setter setCount(c => c + 1) arbeitet jeder Aufruf mit dem AKTUELLEN Wert.

Lazy Initial: useState(() => teuer()) statt `useState(teuer())`.

Wenn der Initial-Wert teuer zu berechnen ist (z.B. localStorage lesen, große Datenstruktur), als Funktion übergeben. Sonst läuft die Berechnung bei JEDEM Render — der Rückgabewert wird zwar nur beim Mount genutzt, die Berechnung läuft aber trotzdem.

setState mit Funktion als Wert: in Wrapper packen.

setFn(myFn) würde myFn() aufrufen (als Updater interpretiert). Wer eine FUNKTION speichern will: setFn(() => myFn) — die äußere Lambda gibt die innere Funktion zurück.

Object.is-Vergleich entscheidet über Re-Render.

React rendert nicht neu, wenn Object.is(neu, alt) true ist. Bei Primitives reicht der Wert, bei Objekten/Arrays MUSS eine neue Referenz. setState(state) mit gleicher Referenz ist no-op.

State von Props ableiten — Anti-Pattern.

useState(props.value) setzt den State NUR beim ersten Mount. Späteres Props-Update wird ignoriert. Wer das will: Wert direkt im Render benutzen, oder key-Prop am Komponenten-Aufruf nutzen für Re-Mount.

setState in `useEffect` kann Infinite-Loop erzeugen.

useEffect(() => setX(y)) ohne Dependency-Array läuft nach jedem Render, jedes setX triggert neuen Render. Lösung: Dependency-Array mit [y], oder die Logik außerhalb des Effects platzieren.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Hooks

Zur Übersicht