9 Der Lebenszyklus von Funktionskomponenten – wie React denkt und arbeitet

React verfolgt eine radikale Idee: Komponenten sind keine Objekte mit Zustand, sondern Funktionen, die eine Beschreibung der Benutzeroberfläche zurückgeben. Diese Funktionen werden nicht einmal ausgeführt und bleiben dann aktiv – sie werden bei Bedarf wiederholt aufgerufen, möglicherweise Dutzende oder Hunderte Male während der Lebensdauer einer Anwendung. Jeder Aufruf ist eine neue Momentaufnahme, ein frisches Bild davon, wie die UI basierend auf den aktuellen Daten aussehen soll.

Diese Denkweise unterscheidet sich fundamental von traditionellen UI-Frameworks, wo Komponenten initialisiert werden und dann über Methoden aktualisiert werden. In React gibt es keine update()-Methode. Es gibt nur die Komponenten-Funktion, die React immer wieder aufruft – und aus diesen wiederholten Aufrufen entsteht die Illusion einer persistenten, sich verändernden Benutzeroberfläche.

9.1 Das deklarative Paradigma verstehen

Deklarative Programmierung bedeutet: Wir beschreiben das “Was”, nicht das “Wie”. Wir sagen nicht “füge ein Element zum DOM hinzu, setze seine Klasse auf ‘active’, entferne das vorherige Element” – sondern wir beschreiben einfach, wie das UI aussehen soll, und React kümmert sich um die Schritte, die nötig sind, um diesen Zustand zu erreichen.

function Counter() {
  const [count, setCount] = useState(0);
  
  // Wir beschreiben, wie das UI aussieht
  // Nicht, wie es von einem Zustand in den anderen kommt
  return (
    <div>
      <p>Zähler: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Erhöhen
      </button>
    </div>
  );
}

Bei jedem Klick ruft React die Counter-Funktion erneut auf. Die Funktion läuft von oben nach unten durch, liest den aktuellen count-Wert, und gibt eine neue JSX-Beschreibung zurück. Diese Beschreibung ist nicht das tatsächliche DOM – es ist eine leichtgewichtige Datenstruktur, die React mit dem vorherigen Ergebnis vergleichen kann.

Der Counter “weiß” nicht, dass er gerade aktualisiert wird. Er hat keine Erinnerung an vorherige Aufrufe. Jeder Durchlauf ist eine isolierte Ausführung der Funktion mit den aktuellen Props und State-Werten. Die Kontinuität entsteht durch React’s interne Zustandsverwaltung, nicht durch die Komponente selbst.

9.2 Die Auslöser für Re-Renderings

Eine Komponente wird neu gerendert, wenn sich etwas geändert hat, das ihre Ausgabe beeinflussen könnte. React kennt vier Hauptgründe für Re-Renderings:

State-Änderungen sind der offensichtlichste Trigger. Ein Aufruf von setState signalisiert React: “Diese Komponente hat neue Daten, sie muss neu gerendert werden.”

const [count, setCount] = useState(0);
setCount(count + 1);  // Plant ein Re-Rendering

Props-Änderungen erfolgen, wenn die Parent-Komponente der Komponente neue Props übergibt. React vergleicht die neuen Props nicht tief mit den alten – es rendert die Komponente neu, sobald die Parent-Komponente sie mit potenziell neuen Props aufruft.

function Parent() {
  const [data, setData] = useState("initial");
  
  return <Child value={data} />;  // Neues data → Child rendert neu
}

function Child({ value }: { value: string }) {
  return <div>{value}</div>;
}

Context-Änderungen triggern Re-Renderings in allen Komponenten, die diesen Context konsumieren – unabhängig davon, wo sie im Komponentenbaum stehen.

function ConsumerComponent() {
  const theme = useContext(ThemeContext);
  // Rendert neu, wenn sich ThemeContext ändert
  return <div className={theme}>Content</div>;
}

Parent-Re-Renderings ziehen standardmäßig alle Child-Komponenten mit. Wenn eine Parent-Komponente neu rendert, rendert React auch alle ihre Children neu – selbst wenn deren Props identisch geblieben sind.

function Parent() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>{count}</p>
      <Child />  {/* Rendert bei jedem Parent-Rendering neu */}
    </div>
  );
}

Diese letzte Regel überrascht oft. Logisch betrachtet bräuchte Child kein Re-Rendering, wenn es keine Props hat oder sich diese nicht geändert haben. Aber React’s Standard-Strategie ist konservativ: Im Zweifel neu rendern. Optimierungen wie React.memo können dieses Verhalten ändern – darauf kommen wir später zurück.

9.3 Der Drei-Phasen-Zyklus: Render, Commit, Paint

Jedes Re-Rendering durchläuft drei distinkte Phasen, die jeweils unterschiedliche Aufgaben erfüllen und verschiedene Garantien bieten.

9.3.1 Phase 1: Render

In der Render-Phase ruft React die Komponenten-Funktionen auf. Alle beteiligten Komponenten – die Komponente selbst und alle Children, die neu gerendert werden müssen – werden als Funktionen ausgeführt. Diese Phase ist rein berechnend: React sammelt Informationen darüber, wie das UI aussehen soll, ohne irgendetwas am tatsächlichen DOM zu ändern.

Während der Render-Phase dürfen keine Side Effects ausgeführt werden. Kein Datenladen, keine Timer setzen, keine Subscriptions starten. Die Render-Phase kann von React mehrfach durchlaufen werden oder abgebrochen werden – etwa in Concurrent Mode. Code, der nur manchmal läuft oder mehrfach ausgeführt wird, würde zu inkonsistentem Verhalten führen.

function Component({ data }: { data: string }) {
  // Render-Phase: Diese Funktion läuft
  const result = expensiveCalculation(data);  // OK: Reine Berechnung
  
  // NICHT OK: Side Effect während Rendering
  // fetch('/api/data');  // ❌ Würde bei jedem Render laufen
  
  return <div>{result}</div>;
}

Das Ergebnis der Render-Phase ist ein Virtual DOM Tree – eine leichtgewichtige Datenstruktur, die das gewünschte UI beschreibt. Dieser Baum existiert nur im Speicher und ist billig zu erstellen und zu vergleichen.

9.3.2 Phase 2: Commit

In der Commit-Phase nimmt React den neuen Virtual DOM Tree und vergleicht ihn mit dem vorherigen. Dieser Vergleich – das “Diffing” – identifiziert die minimalen Änderungen, die am echten DOM vorgenommen werden müssen.

React wendet diese Änderungen dann synchron an. Elemente, die entfernt wurden, werden aus dem DOM gelöscht. Neue Elemente werden eingefügt. Geänderte Attribute werden aktualisiert. Diese Phase ist schnell und atomar – entweder werden alle Änderungen angewendet oder keine.

Nach den DOM-Updates, aber noch vor dem Browser-Paint, führt React alle useLayoutEffect-Hooks aus. Diese laufen synchron und können das Layout lesen oder ändern, bevor der Browser das Ergebnis zeichnet.

9.3.3 Phase 3: Paint

Der Browser übernimmt jetzt. Er berechnet das Layout, rendert die Pixel und zeigt das Ergebnis auf dem Bildschirm. Diese Phase liegt außerhalb von React’s Kontrolle – sie ist Teil des Browser-Rendering-Prozesses.

Nach dem Paint führt React alle useEffect-Hooks aus. Diese laufen asynchron und blockieren nicht den Paint. Der Benutzer sieht bereits die aktualisierte UI, während die Effects im Hintergrund laufen.

Phase Was passiert Side Effects erlaubt? Blockiert UI?
Render Funktionen ausführen, VDOM erstellen Nein Nein
Commit DOM aktualisieren, useLayoutEffect Ja (in useLayoutEffect) Ja
Paint Browser zeichnet, useEffect Ja (in useEffect) Nein

9.4 Zustand als Snapshot

Ein fundamentales Konzept in React: State-Werte sind Snapshots. Wenn eine Komponente rendert, “sieht” sie eine bestimmte Version aller State-Werte. Diese Werte bleiben während des gesamten Renderings konstant – selbst wenn währenddessen setState aufgerufen wird.

function Counter() {
  const [count, setCount] = useState(0);
  
  function handleClick() {
    console.log('Vor Update:', count);  // z.B. 0
    setCount(count + 1);
    console.log('Nach Update:', count);  // Immer noch 0!
    // Der neue Wert ist erst beim nächsten Render verfügbar
  }
  
  return (
    <div>
      <p>{count}</p>
      <button onClick={handleClick}>Klick</button>
    </div>
  );
}

Jeder Render hat seine eigene “Version” der State-Variablen. Diese Version ist während des gesamten Renderings konstant. Event-Handler, die während dieses Renderings erstellt werden, haben Zugriff auf genau diese Version – nicht auf spätere Updates.

function DelayedUpdate() {
  const [count, setCount] = useState(0);
  
  function handleClick() {
    setTimeout(() => {
      // Diese Funktion "erinnert sich" an den count-Wert
      // zum Zeitpunkt des Klicks
      alert(`Count war: ${count}`);
    }, 3000);
    
    setCount(count + 1);
  }
  
  return <button onClick={handleClick}>{count}</button>;
}

Drei schnelle Klicks erzeugen drei Timeouts. Jedes “sieht” den count-Wert zum Zeitpunkt seines Klicks: 0, 1, 2. Die Alerts zeigen diese Werte, nicht den aktuellsten count-Wert.

Dieses Snapshot-Verhalten erklärt viele anfänglich verwirrende Situationen in React. Es ist kein Bug, sondern ein fundamentales Merkmal der Funktionskomponenten-Architektur. Jeder Render ist eine isolierte Ausführung mit seinen eigenen Props, State und Event-Handlern.

9.5 Der Referenzvergleich und seine Konsequenzen

React entscheidet anhand von Object.is, ob sich ein Wert geändert hat. Für primitive Werte wie Zahlen, Strings und Booleans funktioniert das intuitiv: 5 === 5, "hello" === "hello". Bei Objekten und Arrays wird es komplexer.

const obj1 = { name: "Alice" };
const obj2 = { name: "Alice" };

Object.is(obj1, obj2);  // false - verschiedene Objekte!
Object.is(obj1, obj1);  // true - gleiches Objekt

Zwei Objekte mit identischem Inhalt sind verschiedene Objekte, wenn sie separat erstellt wurden. Diese Referenz-Semantik hat direkte Auswirkungen auf React’s Re-Rendering-Verhalten.

function Parent() {
  const [trigger, setTrigger] = useState(0);
  
  // Neues Objekt bei jedem Render!
  const config = { theme: "dark" };
  
  return <Child config={config} />;
}

const Child = React.memo(({ config }: { config: { theme: string } }) => {
  console.log('Child rendert');
  return <div>{config.theme}</div>;
});

Obwohl Child mit React.memo optimiert ist, rendert es bei jedem Parent-Rendering neu. config ist ein neues Objekt mit jeder Ausführung von Parent – und React.memo sieht eine Änderung, weil die Referenz verschieden ist.

Die Lösung liegt in stabilen Referenzen – entweder durch Definition außerhalb der Komponente, useMemo für berechnete Werte, oder sorgfältige Strukturierung des State.

// Lösung 1: Außerhalb der Komponente
const CONFIG = { theme: "dark" };

function Parent() {
  return <Child config={CONFIG} />;  // Stabile Referenz
}

// Lösung 2: useMemo
function Parent() {
  const config = useMemo(() => ({ theme: "dark" }), []);
  return <Child config={config} />;  // Stabile Referenz
}

9.6 Flüchtigkeit und Persistenz

In einer Funktionskomponente ist alles flüchtig. Variablen, die im Function-Body deklariert werden, existieren nur während der Ausführung. Bei jedem Render werden sie neu erstellt.

function Component() {
  let counter = 0;  // Bei jedem Render wieder 0!
  
  function increment() {
    counter++;
    console.log(counter);  // Funktioniert während dieses Renderings
    // Aber beim nächsten Render ist counter wieder 0
  }
  
  return <button onClick={increment}>{counter}</button>;
}

Diese Flüchtigkeit ist fundamental. Die Komponente ist keine persistente Instanz, sondern eine Funktion, die wiederholt aufgerufen wird. Jeder Aufruf ist ein frischer Start mit neuen lokalen Variablen.

Persistenz – Werte, die zwischen Renderings erhalten bleiben – erfordert spezielle Mechanismen:

useState für reaktive Werte, die das UI beeinflussen:

const [count, setCount] = useState(0);  // Überlebt Renderings

useRef für nicht-reaktive Werte, die keine UI-Updates auslösen:

const timerRef = useRef<number | null>(null);  // Überlebt Renderings

useMemo/useCallback für berechnete Werte oder Funktionen, die stabil bleiben sollen:

const expensiveValue = useMemo(() => calculate(data), [data]);

Diese Hooks sind React’s Antwort auf die Flüchtigkeit von Funktionskomponenten. Sie schaffen Speicherplätze, die React zwischen Renderings verwaltet und bereitstellt.

9.7 Die Render-Kaskade

Ein Re-Rendering pflanzt sich standardmäßig durch den Komponentenbaum fort. Wenn eine Parent-Komponente neu rendert, rendern alle Children mit – und deren Children, und so weiter.

function App() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Update</button>
      <Level1 />  {/* Rendert bei jedem count-Update neu */}
    </div>
  );
}

function Level1() {
  return <Level2 />;  {/* Rendert mit */}
}

function Level2() {
  return <Level3 />;  {/* Rendert mit */}
}

function Level3() {
  return <div>Tief verschachtelt</div>;  {/* Rendert mit */}
}

Jeder Klick auf den Button rendert nicht nur App, sondern auch Level1, Level2 und Level3 neu – obwohl diese Komponenten keine Props haben und sich nichts an ihnen geändert hat.

Diese Kaskade ist React’s Default-Strategie. Die Annahme: Renderings sind billig, und es ist sicherer, zu viel zu rendern als zu wenig. In den meisten Anwendungen ist das auch richtig – moderne Computer können Hunderte von Komponenten pro Frame rendern, ohne dass es spürbar wird.

Probleme entstehen erst bei sehr großen Komponentenbäumen, komplexen Berechnungen während des Renderings, oder sehr häufigen Updates. Dann kommen Optimierungsstrategien ins Spiel: React.memo, useMemo, useCallback und sorgfältiges State-Design.

9.8 Die Strict Mode-Warnung

React’s Strict Mode führt Komponenten in der Entwicklungsumgebung doppelt aus. Jedes Rendering läuft zweimal: einmal normal, einmal als “Test”. Das Ergebnis des zweiten Durchlaufs wird verworfen.

function Component() {
  console.log('Rendering');  // Erscheint zweimal in Development
  return <div>Test</div>;
}

Dieses Verhalten ist kein Bug. Es ist ein Debugging-Tool, das Probleme aufdecken soll. Komponenten sollen idempotent sein – mehrfache Ausführungen mit denselben Inputs sollen dasselbe Ergebnis liefern. Side Effects während des Renderings würden diese Garantie brechen und zu schwer debugbaren Problemen führen.

In Production läuft jedes Rendering nur einmal. Das doppelte Rendering ist ein Development-Feature, das hilft, Probleme frühzeitig zu erkennen.

9.9 Batching: Mehrere Updates, ein Rendering

React ist schlau genug, mehrere State-Updates zu bündeln. Wenn innerhalb derselben Event-Handler-Funktion mehrere setState-Aufrufe erfolgen, werden sie zu einem einzigen Rendering zusammengefasst.

function handleClick() {
  setCount(count + 1);
  setFlag(true);
  setName("Alice");
  // Nur ein Rendering für alle drei Updates
}

Dieses Batching verhindert unnötige Zwischenzustände im UI und verbessert die Performance. Ohne Batching würde die Komponente dreimal rendern – mit möglicherweise inkonsistenten Zwischenzuständen.

In React 18 wurde Batching auf asynchrone Callbacks ausgeweitet:

fetch('/api/data').then(data => {
  setData(data);
  setLoading(false);
  // Gebatched, auch in async Callback
});

setTimeout(() => {
  setCount(count + 1);
  setFlag(true);
  // Gebatched, auch in setTimeout
}, 1000);

Das Verständnis des Lebenszyklus ist fundamental für effektive React-Entwicklung. Die Komponente als wiederholbar ausgeführte Funktion zu begreifen, die Phasen des Rendering-Prozesses zu kennen, und die Snapshot-Natur von State zu verstehen – das sind die Grundlagen, auf denen alle weiteren React-Konzepte aufbauen.