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
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
counter
den 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
// 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
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
counter
aus dem Scope derUseStateExample
Komponente. Initial hatcounter
den Wert 0. setCounter(0 + 1)
wird aufgerufen. React plant nun eine Aktualisierung, um dencounter
auf1
zu setzen. Wichtig ist hier, dass der Wert voncounter
in der JavaScript-Variable innerhalb vonhandleIncrementBad
immer noch0
ist.
- Zu diesem Zeitpunkt liest die Funktion den aktuellen Wert von
- Zweiter Aufruf von
setCounter(counter + 1)
:- Auch dieser Aufruf liest den Wert von
counter
aus dem Scope derUseStateExample
Funktion. Da diesetCounter()
Funktion asynchron ist und die Komponente noch nicht neu gerendert wurde, ist der Wert voncounter
in diesem Scope immer noch0
. JavaScript “erinnert” sich an den Wert voncounter
zum Zeitpunkt der Erstellung deshandleIncrementBad
Funktions-Scopes. setCounter(counter + 1)
wird erneut aufgerufen. React plant nun eine weitere Aktualisierung, um dencounter
auf1
zu 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;