Zum Inhalt springen

7.4 React-Grundlagen: State und der Render-Kreislauf

Zu Zen-Modus wechseln

Nach dieser Einheit kannst du:

  • erklären, warum eine lokale Variable sich nicht als Zustand eignet, und Zustand mit useState verwalten.
  • State nur über den Setter und Arrays und Objekte immutabel aktualisieren, mit der Funktionsform, wenn der neue Wert vom alten abhängt.
  • die Regeln für Hooks nennen und begründen.
  • den Render-Kreislauf beschreiben und entscheiden, ob ein Wert State ist oder beim Rendern berechnet wird.

Ein Kellner mit dem Gedächtnis eines Goldfischs: Nach jeder Runde durchs Lokal hat er alles vergessen. Damit trotzdem keine Bestellung verloren geht, liegt an der Theke ein Notizbuch. Bevor er losgeht, schaut er hinein; ändert sich etwas, trägt er es dort ein und dreht eine neue Runde. Genau so arbeitet eine React-Komponente: Bei jedem Rendern läuft die Funktion von vorn und vergisst alle lokalen Variablen. Das Notizbuch führt React, und der Eintrag darin heißt State.

flowchart LR
    accTitle: React bewahrt den State außerhalb der Funktion auf
    react[("React<br/>Notizbuch: count = 3")] -- "useState liefert 3" --> run["Komponente<br/>läuft von vorn"]
    run -- "setCount(4)" --> react
    run --> ui["Anzeige: 3"]

Abb. 7.14: Die Funktion vergisst bei jedem Durchlauf alles; den State bewahrt React auf (eigene Darstellung).

Props kommen von außen (Einheit 7.3); State ist der Zustand, den eine Komponente selbst besitzt und ändert. Er ist nötig, sobald sich etwas durch Benutzung ändert: der Text im Eingabefeld, das gewählte Preset, der Inhalt des Warenkorbs. Diese Einheit führt useState ein, den ersten Hook, und zeigt den Kreislauf, in dem jede React-Anwendung arbeitet.

  • State: Zustand, den eine Komponente selbst besitzt und der beim Ändern ein neues Rendern auslöst.
  • Rendern: React führt die Komponentenfunktion aus und erhält das JSX für den aktuellen Stand.
  • Setter: die Funktion aus useState, über die der State geändert wird, etwa setCount.
  • Immutabel aktualisieren: ein Array oder Objekt nicht verändern, sondern durch eine geänderte Kopie ersetzen.
  • Hook: eine Funktion mit use-Präfix, über die Komponenten React-Mechanismen wie State nutzen.

Eine Auswahl, die beim Klick auf “Next preset” das nächste Render-Preset zeigen soll. Der Versuch mit einer gewöhnlichen Variablen, und warum er nicht funktioniert:

const presets = ["web-1080p", "web-4k", "mobile-720p"];
export function PresetPicker() {
let index = 0; // a plain local variable
function handleClick() {
index = (index + 1) % presets.length; // changes the variable ...
}
return (
<>
<button onClick={handleClick}>Next preset</button>
<p>Preset: {presets[index]}</p>
</>
);
}

Beispielprojekt: 07_react_grundlagen_examples/ex12_plain_variable

Der Klick ändert index tatsächlich, sichtbar wird davon nichts. Zwei Gründe:

  1. Lokale Variablen überleben das Rendern nicht. Rendert React die Komponente erneut, läuft die Funktion von vorn, und index startet wieder bei 0.
  2. Die Änderung löst kein Rendern aus. React erfährt nichts davon und hat keinen Grund, die Anzeige zu erneuern.
sequenceDiagram
    accTitle: Eine lokale Variable überlebt das Rendern nicht
    participant user as Benutzer
    participant comp as PresetPicker
    participant react as React
    react->>comp: Rendern: index = 0
    user->>comp: Klick
    comp->>comp: index = 1, kein Rendern
    react->>comp: späteres Rendern: index = 0

Abb. 7.15: Die Änderung bleibt unsichtbar und ist beim nächsten Rendern wieder weg (eigene Darstellung).

Damit eine Änderung sichtbar wird, braucht es also beides: Der Wert muss erhalten bleiben, und das Rendern muss ausgelöst werden. Genau dafür gibt es useState.

import { useState } from "react";
export function PresetPicker() {
const [index, setIndex] = useState(0); // state variable + setter
function handleClick() {
setIndex((current) => (current + 1) % presets.length);
}
return (
<>
<button onClick={handleClick}>Next preset</button>
<p>Preset: {presets[index]}</p>
</>
);
}

Beispielprojekt: 07_react_grundlagen_examples/ex13_use_state

useState(startwert) liefert ein Array mit genau zwei Einträgen, das per Destructuring entpackt wird: den aktuellen Wert und eine Setter-Funktion. Die Namenskonvention ist [something, setSomething]. Den Typ leitet TypeScript aus dem Startwert ab (useState(0) ergibt number); nur wenn der Startwert nichts verrät, gibst du ihn an: useState<Job[]>([]) oder useState<Job | null>(null).

sequenceDiagram
    accTitle: useState über zwei Renderdurchgänge
    participant comp as PresetPicker
    participant react as React
    comp->>react: 1. Rendern: useState(0)
    react-->>comp: [0, setIndex]
    Note over comp: Klick
    comp->>react: setIndex(1)
    Note over react: merkt sich 1, plant neues Rendern
    comp->>react: 2. Rendern: useState(0)
    react-->>comp: [1, setIndex]

Abb. 7.16: Der Startwert zählt nur beim ersten Rendern; danach liefert React den gemerkten Wert (eigene Darstellung).

So arbeitet das zusammen:

  1. Erstes Rendern. useState(0) liefert [0, setIndex]. React merkt sich: Der Wert ist 0.
  2. Aktion. Der Klick ruft setIndex(...). React notiert den neuen Wert 1 und plant ein neues Rendern.
  3. Zweites Rendern. Im Code steht weiterhin useState(0), React weiß aber, dass der Wert inzwischen 1 ist, und liefert [1, setIndex].

Der Startwert wird also nur beim ersten Rendern verwendet; danach gewinnt das, was React sich gemerkt hat.

  1. Zustand wird nie direkt geändert, nur über den Setter. index = 2 bewirkt nichts Sichtbares; erst setIndex(2) löst das Neu-Rendern aus.
  2. Objekte und Arrays werden ersetzt, nicht verändert. React erkennt Änderungen daran, dass ein anderes Objekt im State steht; list.push(item) bleibt unsichtbar, weil das Array dasselbe bleibt. Richtig ist die Kopie mit Spread: setJobs([...jobs, newJob]) beziehungsweise setJob({ ...job, state: "done" }).
  3. Folgt der neue Wert aus dem alten, nimmt der Setter eine Funktion: setCount((current) => current + 1).

Zur dritten Regel gehört ein Beispiel, weil sie sonst wie Geschmackssache wirkt:

// two increments in one handler:
setCount(count + 1);
setCount(count + 1); // count is the value of THIS render: +1 in total
setCount((c) => c + 1);
setCount((c) => c + 1); // each call gets the latest value: +2 in total

Beispielprojekt: 07_react_grundlagen_examples/ex14_setter_forms

Genauer erklärt: count ist innerhalb eines Renderdurchgangs eine Konstante; beide Zeilen rechnen mit demselben alten Wert, die zweite überschreibt die erste. Die Funktionsform bekommt dagegen von React jeweils den aktuellen Stand. Dasselbe Problem entsteht in setTimeout-Callbacks und nach await: Dort ist der eingefangene Wert oft schon veraltet. Deshalb gilt: Hängt der neue Wert vom alten ab, immer die Funktionsform.

Häufig ist der Zustand keine einzelne Zahl, sondern eine Liste. Genau hier greift Regel 2:

function TagList() {
const [tags, setTags] = useState<string[]>(["draft", "urgent"]);
const addTag = (tag: string) => setTags([...tags, tag]); // append
const removeTag = (tag: string) => setTags(tags.filter((t) => t !== tag)); // remove
const clearTags = () => setTags([]); // replace
return (
<>
<ul>
{tags.map((tag) => (
<li key={tag}>
{tag} <button onClick={() => removeTag(tag)}>x</button>
</li>
))}
</ul>
<button onClick={() => addTag(`tag-${tags.length + 1}`)}>add</button>
<button onClick={clearTags}>clear</button>
</>
);
}

Beispielprojekt: 07_react_grundlagen_examples/ex15_tag_list

[...tags, tag], tags.filter(...) und [] haben eines gemeinsam: Sie erzeugen ein neues Array, statt das alte zu verändern. Nur so erkennt React die Änderung und rendert die Liste neu.

flowchart LR
    accTitle: Verändern gegen Ersetzen eines Arrays im State
    old["tags<br/>Array A"] -- "tags.push(x)" --> same["immer noch Array A<br/>React: nichts geändert"]
    old -- "[...tags, x]" --> neu["neues Array B<br/>React: neu rendern"]

Abb. 7.17: React vergleicht, ob ein anderes Array im State steht, nicht, ob sich sein Inhalt geändert hat (eigene Darstellung).

Auflösung

Mit count + 1 zeigt er 6: Alle drei Zeilen rechnen mit der Konstante count = 5 dieses Renderdurchgangs und setzen dreimal 6. Mit der Funktionsform zeigt er 8: Jeder Aufruf bekommt von React den Stand nach dem vorherigen, also 6, 7, 8.

Zustand gehört zu einer Instanz einer Komponente auf dem Bildschirm. Wer dieselbe Komponente zweimal rendert, bekommt zwei voneinander völlig unabhängige Zustände:

<PresetPicker /> {/* has its own index */}
<PresetPicker /> {/* has its own index, too */}

Ein Klick in der einen Auswahl lässt die andere unberührt. Das ist der Grund, warum Komponenten wiederverwendbar sind, ohne sich gegenseitig zu stören, und zugleich der Grund, warum gemeinsam genutzter Zustand nach oben wandern muss (Einheit 7.5).

Wo begegnet dir das? Auf einer Seite mit mehreren eingebetteten Videoplayern hat jeder Player seine eigene Abspielposition und Lautstärke. Pausierst du den einen, läuft der andere weiter: ein Baustein, mehrere unabhängige Zustände.

useState ist der erste Hook. Für alle Hooks gelten zwei Regeln:

  • Nur auf der obersten Ebene aufrufen, nie in if, in Schleifen oder in verschachtelten Funktionen. React unterscheidet die Hooks einer Komponente über ihre Aufrufreihenfolge; wird ein Aufruf einmal übersprungen, verrutscht die Zuordnung, und der zweite Hook bekommt den Wert des ersten.
  • Nur in Komponenten oder in eigenen Hooks aufrufen, nicht in gewöhnlichen Funktionen.

Die Vite-Vorlage prüft beide Regeln mit dem Linter: npm run lint startet oxlint, und die Regel react/rules-of-hooks meldet einen Hook in einer Bedingung, bevor die Seite überhaupt läuft.

flowchart LR
    accTitle: Hooks werden über ihre Reihenfolge zugeordnet
    subgraph r1["1. Rendern"]
        direction TB
        h1["Hook 1: filename"] --> h2["Hook 2: preset"]
    end
    subgraph r2["2. Rendern, Hook 1 im if übersprungen"]
        direction TB
        h2b["erster Aufruf: preset<br/>bekommt Wert von filename"]
    end
    r1 --> r2

Abb. 7.18: Fällt ein Hook-Aufruf weg, bekommt der nächste den falschen Platz im Notizbuch (eigene Darstellung).

Praktisch heißt das: Hooks stehen ganz oben in der Komponente, so wie import-Zeilen ganz oben in der Datei stehen. Brauchst du einen Zustand nur unter einer Bedingung, kommt trotzdem der Hook nach oben und die Bedingung ins JSX.

Learn useState In 15 Minutes von Web Dev Simplified (Englisch):

Abspielen

Alles bisher Gezeigte läuft in einem einzigen, immer gleichen Kreislauf ab. Wer ihn einmal verstanden hat, kann die meisten React-Fragen selbst beantworten:

flowchart LR
    accTitle: Der Render-Kreislauf vom Ereignis zur Anzeige
    event["Ereignis<br/>(Klick, Eingabe)"] --> handler["Handler ruft Setter"]
    handler --> schedule["React plant<br/>neues Rendern"]
    schedule --> render["Komponente läuft<br/>erneut durch"]
    render --> jsx["neues JSX"]
    jsx --> diff["Vergleich mit<br/>dem alten Stand"]
    diff --> dom["minimale Änderung<br/>am echten DOM"]

Abb. 7.19: Ereignis, Setter, Rendern, Vergleich, DOM-Änderung, und wieder von vorn (eigene Darstellung).

Drei Folgerungen daraus beantworten erfahrungsgemäß viele Fragen:

  • “Rendern” heißt: Die Funktion läuft erneut von oben durch. Jede Berechnung darin wird neu ausgeführt, jede im Rumpf definierte Funktion neu erzeugt. Nur der State überlebt, weil React ihn außerhalb aufbewahrt.
  • Der Setter wirkt nicht sofort. Direkt nach setCount(5) steht in count immer noch der alte Wert; der neue gilt erst im nächsten Renderdurchgang. Ein console.log(count) unmittelbar nach dem Setter hilft beim Debuggen deshalb nicht weiter.
  • Was sich aus State berechnen lässt, ist kein State. Die Anzeige “3 von 8”, die Anzahl der fehlerhaften Jobs, die Warnfarbe ab 260 Zeichen: All das wird beim Rendern ausgerechnet. Ein zusätzlicher State dafür wäre eine zweite Wahrheit, die irgendwann von der ersten abweicht.
Auflösung

State: die Liste der Aufträge, der Suchtext und der aufgeklappte Auftrag; sie entstehen durch Daten oder Benutzeraktionen und lassen sich aus nichts anderem berechnen. Berechnet: die gefilterte Liste (Liste plus Suchtext) und die Zahl der fehlgeschlagenen Aufträge (aus der Liste).

  1. State direkt verändert: jobs.push(...) oder job.state = "done" ändert die Daten, aber nicht die Anzeige. React braucht ein neues Array oder Objekt.
  2. Abgeleitete Werte im State: ein zusätzlicher failedCount-State neben der Jobliste. Zwei Wahrheiten, die auseinanderlaufen, sobald eine Aktualisierung vergessen wird.
  3. Den Setter-Wert sofort lesen: setCount(5); console.log(count); zeigt den alten Wert.
  4. count + 1 statt Funktionsform: Mehrere Setter-Aufrufe in einem Handler rechnen alle mit demselben alten Wert.
  5. Hook in einer Bedingung: if (x) { const [a, setA] = useState(0); } bricht die Aufrufreihenfolge. npm run lint meldet das vorab (Regel react/rules-of-hooks), React selbst erst zur Laufzeit.

Du kannst dir diese Einheit kostenlos mit der Vorlesefunktion deines Browsers oder Betriebssystems vorlesen lassen (etwa „Laut vorlesen” in Edge), ohne Konto und ohne Upload; die Codebeispiele liest du dabei am Bildschirm mit. Freiwillig kannst du mit Google NotebookLM (Google-Konto nötig) aus diesem öffentlichen Text eine gesprochene Zusammenfassung erzeugen, auch auf Deutsch.

Du kannst jetzt:

  • erklären, warum eine lokale Variable sich nicht als Zustand eignet, und Zustand mit useState verwalten.
  • State nur über den Setter und Arrays und Objekte immutabel aktualisieren, mit der Funktionsform, wenn der neue Wert vom alten abhängt.
  • die Regeln für Hooks nennen und begründen.
  • den Render-Kreislauf beschreiben und entscheiden, ob ein Wert State ist oder beim Rendern berechnet wird.
  • Aufgabe 07-4a - State-Training, Stufe Wesentlich_überwiegend
  • useState, Startwert, Setter → react.dev: State: a component’s memory, useState
  • State als Momentaufnahme, Funktionsform des Setters → react.dev: State as a snapshot, Queueing a series of state updates
  • Arrays im State → react.dev: Updating arrays in state
  • Regeln für Hooks → react.dev: Rules of Hooks
  • oxlint mit react/rules-of-hooks in der Vite-Vorlage → eigener Test mit npm create vite@latest (Vite 8.3.4, Oktober 2026)
  • Ergebnis 6 und 8 bei drei Setter-Aufrufen → eigener Test im Beispielprojekt ex14_setter_forms (Oktober 2026)
  • Video Learn useState In 15 Minutes → Web Dev Simplified auf YouTube: O6P86uwfdR0
  • Abb. 7.14 (Notizbuch), Abb. 7.15 (lokale Variable), Abb. 7.16 (useState über zwei Durchgänge), Abb. 7.17 (Verändern gegen Ersetzen), Abb. 7.18 (Hook-Reihenfolge), Abb. 7.19 (Render-Kreislauf) → eigene Darstellung, alle als Mermaid-Diagramm
  • Oliver Maurhart (MAO)
  • Michael Gabriel (GAM)

CC-BY-SA

https://qiz.at/rules