State
Bisher empfangen unsere Komponenten Daten von außen (Props) und rendern sie. Aber was, wenn eine Komponente eigene Daten verwalten und verändern soll? Zum Beispiel ein aufklappbares Element, das sich den Zustand „offen/geschlossen” merkt.
Das Problem: Lokale Variablen reichen nicht
Abschnitt betitelt „Das Problem: Lokale Variablen reichen nicht“Versuchen wir es naiv — mit einer normalen Variable:
function Collapsible({ titel, children }) { let isOpen = false;
function handleClick() { isOpen = !isOpen; console.log("isOpen:", isOpen); // ändert sich im Log... }
return ( <div> <button onClick={handleClick}>{titel}</button> {isOpen && <div>{children}</div>} </div> );}Klickst du auf den Button, siehst du in der Konsole, dass isOpen sich ändert — aber die UI aktualisiert sich nicht. Warum?
Zwei Gründe:
- Lokale Variablen überleben kein Re-Render. Jedes Mal, wenn React die Komponente neu rendert, wird die Funktion erneut aufgerufen — und
isOpenwird wieder auffalsegesetzt. - React weiß nicht, dass sich etwas geändert hat. Das Ändern einer lokalen Variable löst kein Re-Render aus. React beobachtet deine Variablen nicht — es braucht einen expliziten Mechanismus dafür.
Der Weg über Klassen-Komponenten
Abschnitt betitelt „Der Weg über Klassen-Komponenten“Bevor wir zur modernen Lösung kommen, lohnt sich ein Blick auf die historische Variante — Klassen-Komponenten. Sie machen das zugrunde liegende Problem sichtbar.
Eine Klasse erzeugt Instanzen. Jedes <Collapsible> im JSX ist eine eigene Instanz, die eigene Daten (Instance Members) haben kann. Damit hat die Komponente ein „Gedächtnis”, das zwischen den Rendervorgängen bestehen bleibt:
class Collapsible extends React.Component { constructor(props) { super(props); this.state = { isOpen: false }; }
render() { return ( <div> <button onClick={() => this.setState({ isOpen: !this.state.isOpen })}> {this.props.titel} </button> {this.state.isOpen && <div>{this.props.children}</div>} </div> ); }}Hier ist this.state ein Instanz-Member — er gehört zu dieser einen Instanz und überlebt Re-Renders. Aber es gibt eine Regel: Du darfst this.state nicht direkt verändern. Stattdessen sagst du React über this.setState(), dass du einen neuen Zustand willst.
Warum? Weil React sonst nicht weiß, dass sich etwas geändert hat. this.state.isOpen = true ändert den Wert still — ohne dass React davon erfährt und die UI aktualisiert. this.setState() ist der Weg, React mitzuteilen: „Hier hat sich etwas geändert, bitte neu rendern.”
State-Updates: Merging
Abschnitt betitelt „State-Updates: Merging“this.setState() ersetzt nicht den gesamten State — es merged das übergebene Objekt mit dem bestehenden State:
class Formular extends React.Component { constructor(props) { super(props); this.state = { name: "", email: "", istAbgeschickt: false, }; }
render() { if (this.state.istAbgeschickt) { return <p>Danke, {this.state.name}!</p>; }
return ( <form onSubmit={(event) => { event.preventDefault(); this.setState({ istAbgeschickt: true }); // name und email bleiben erhalten! }}> <input value={this.state.name} onChange={(e) => this.setState({ name: e.target.value })} placeholder="Name" /> <input value={this.state.email} onChange={(e) => this.setState({ email: e.target.value })} placeholder="E-Mail" /> <button type="submit">Absenden</button> </form> ); }}this.setState({ istAbgeschickt: true }) ändert nur istAbgeschickt. Die Felder name und email bleiben unverändert — React merged das neue Objekt in den bestehenden State.
Immutability
Abschnitt betitelt „Immutability“Wichtig dabei: Du änderst den State nie direkt. Das ist das Prinzip der Immutability (Unveränderlichkeit):
// ❌ Falsch — State direkt mutierenthis.state.name = "Dan";
// ✅ Richtig — neuen State über setState setzenthis.setState({ name: "Dan" });React vergleicht den alten mit dem neuen State, um zu entscheiden, was sich geändert hat und was neu gerendert werden muss. Wenn du den alten State direkt veränderst, kann React den Unterschied nicht erkennen.
State in Funktionskomponenten: useState
Abschnitt betitelt „State in Funktionskomponenten: useState“Klassen-Komponenten funktionieren, aber sie sind wortreich. Funktionen haben keine Instanzen — also auch keine Instanz-Member, die Daten zwischen Renders speichern könnten.
Seit React 16.8 gibt es dafür einen eleganten Mechanismus: Hooks. Der Hook useState gibt einer Funktionskomponente etwas, das sie von Natur aus nicht hat — eine Art „Instanz-State”, der von React extern verwaltet wird.
import { useState } from "react";
function Collapsible({ titel, children }) { const [isOpen, setIsOpen] = useState(false);
return ( <div> <button onClick={() => setIsOpen(!isOpen)}> {titel} </button> {isOpen && <div>{children}</div>} </div> );}useState(false) gibt ein Array mit zwei Einträgen zurück:
- Der aktuelle Wert — beim ersten Render der Initialwert (
false), danach der zuletzt gesetzte Wert. - Eine Setter-Funktion — ruft React auf, den Wert zu ändern und die Komponente neu zu rendern.
Array-Destructuring
Abschnitt betitelt „Array-Destructuring“Die [isOpen, setIsOpen]-Syntax ist Array-Destructuring. useState gibt ein Array zurück, und du entpackst es in zwei benannte Variablen. Du könntest auch schreiben:
const stateArray = useState(false);const isOpen = stateArray[0];const setIsOpen = stateArray[1];Aber das ist umständlich. Die Destructuring-Variante ist kürzer, und du kannst die Variablen frei benennen — sie müssen nicht isOpen heißen. Die Konvention ist [wert, setWert].
Der Initialwert wird nur einmal verwendet
Abschnitt betitelt „Der Initialwert wird nur einmal verwendet“React verwendet den Initialwert von useState nur beim allerersten Render. Bei jedem weiteren Render wird der Parameter ignoriert — React kennt den aktuellen Wert bereits.
Das wird sichtbar, wenn du eine Funktion als Initialwert übergibst:
function Collapsible({ titel, children }) { const [isOpen, setIsOpen] = useState(() => { console.log("Initialwert wird berechnet"); return false; });
console.log("Komponente wird gerendert");
return ( <div> <button onClick={() => setIsOpen(!isOpen)}> {titel} </button> {isOpen && <div>{children}</div>} </div> );}In der Konsole siehst du:
Initialwert wird berechnet ← nur beim ersten RenderKomponente wird gerendert ← bei jedem RenderNach dem ersten Render erscheint nur noch "Komponente wird gerendert". Die Initialisierungsfunktion wird nie wieder aufgerufen. Das ist besonders nützlich, wenn der Initialwert teuer zu berechnen ist — zum Beispiel beim Lesen aus dem localStorage:
const [einstellungen, setEinstellungen] = useState(() => { return JSON.parse(localStorage.getItem("settings")) || {};});Ohne die Funktion würde JSON.parse(...) bei jedem Render ausgeführt — das Ergebnis aber nach dem ersten Mal verworfen. Mit der Funktion passiert die Berechnung nur einmal.
Mehrere State-Werte
Abschnitt betitelt „Mehrere State-Werte“Anders als bei Klassen-Komponenten, wo der gesamte State in einem Objekt lebt, verwendest du in Funktionskomponenten einen eigenen useState-Aufruf pro Wert:
function AnmeldeFormular() { const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [istAbgeschickt, setIstAbgeschickt] = useState(false);
if (istAbgeschickt) { return <p>Danke, {name}!</p>; }
return ( <form onSubmit={(event) => { event.preventDefault(); setIstAbgeschickt(true); }}> <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" /> <input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="E-Mail" /> <button type="submit">Absenden</button> </form> );}Jeder useState-Aufruf verwaltet genau einen Wert — unabhängig von den anderen. Es gibt kein Merging wie bei this.setState(). Wenn du setName("Dan") aufrufst, ändert sich nur name. Die anderen Werte bleiben, wie sie sind — nicht weil React merged, sondern weil jeder Hook seinen eigenen, isolierten Speicher hat.
Setter mit Funktion: setX(state => newState)
Abschnitt betitelt „Setter mit Funktion: setX(state => newState)“Neben dem direkten Übergeben eines neuen Wertes kannst du dem Setter auch eine Funktion übergeben. React ruft diese Funktion mit dem aktuellen State-Wert auf und verwendet den Rückgabewert als neuen State:
// Direkter WertsetZaehler(zaehler + 1);
// Funktion — erhält den aktuellen Wert als ArgumentsetZaehler(aktuell => aktuell + 1);Beide Varianten sind oft gleichwertig. Der Unterschied zeigt sich, wenn mehrere State-Updates hintereinander ausgelöst werden. React batched Updates — mehrere setState-Aufrufe in einem Event-Handler werden zusammengefasst und führen zu nur einem Re-Render. Dabei kann die direkte Variante zu veralteten Werten führen, weil alle Aufrufe denselben zaehler-Wert aus der aktuellen Render-Closure lesen:
// ❌ Beide lesen denselben Wert — Ergebnis: +1 statt +2setZaehler(zaehler + 1);setZaehler(zaehler + 1);
// ✅ Jede Funktion erhält den jeweils aktuellsten Wert — Ergebnis: +2setZaehler(aktuell => aktuell + 1);setZaehler(aktuell => aktuell + 1);Als Faustregel: Wenn der neue State vom aktuellen State abhängt, verwende die Funktions-Variante.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“| Konzept | Beschreibung |
|---|---|
| Lokale Variablen | Überleben kein Re-Render und lösen keins aus |
this.setState() | Klassen-Komponenten: merged neuen State, löst Re-Render aus |
| Immutability | State nie direkt ändern — immer über Setter-Funktionen |
useState(initialwert) | Hook für Funktionskomponenten: gibt [wert, setWert] zurück |
| Initialwert | Wird nur beim ersten Render verwendet — Funktion für teure Berechnungen |
| Mehrere Hooks | Ein useState pro Wert — kein Merging, jeder Hook ist isoliert |
| Setter mit Funktion | setX(aktuell => aktuell + 1) — sicher wenn neuer State vom aktuellen abhängt |