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
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
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?
- React merkt sich die Änderung:
setCounter(counter + 1)teilt React mit, dass sich der State geändert hat. - Re-Rendering wird ausgelöst: React render die Komponente neu.
- Neuer Wert wird verwendet: Beim nächsten Render hat
counterden neuen Wert. - 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
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
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
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
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:
- Erster Aufruf von
setCounter(counter + 1):- Zu diesem Zeitpunkt liest die Funktion den aktuellen Wert von
counteraus dem Scope derUseStateExampleKomponente. Initial hatcounterden Wert 0. setCounter(0 + 1)wird aufgerufen. React plant nun eine Aktualisierung, um dencounterauf1zu setzen. Wichtig ist hier, dass der Wert voncounterin der JavaScript-Variable innerhalb vonhandleIncrementBadimmer noch0ist.
- Zu diesem Zeitpunkt liest die Funktion den aktuellen Wert von
- Zweiter Aufruf von
setCounter(counter + 1):- Auch dieser Aufruf liest den Wert von
counteraus dem Scope derUseStateExampleFunktion. Da diesetCounter()Funktion asynchron ist und die Komponente noch nicht neu gerendert wurde, ist der Wert voncounterin diesem Scope immer noch0. JavaScript "erinnert" sich an den Wert voncounterzum Zeitpunkt der Erstellung deshandleIncrementBadFunktions-Scopes. setCounter(counter + 1)wird erneut aufgerufen. React plant nun eine weitere Aktualisierung, um dencounterauf1zu setzen.
- Auch dieser Aufruf liest den Wert von
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.
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
- useState – react.dev
- State as a Snapshot – react.dev
- Queueing a Series of State Updates – react.dev
- Updating Objects in State – react.dev