1. React Rendering-Lifecycle im Überblick

Jede Komponente durchläuft denselben Grundzyklus: Trigger → Render-Phase → Commit-Phase. Bei Zustandsänderungen wird dieser Zyklus wiederholt (Update), beim Entfernen aus dem Baum wird aufgeräumt (Unmount).

Trigger
Initial-Mount oder State-/Prop-Änderung
Render-Phase
Funktionskörper wird ausgeführt (rein, ohne Seiteneffekte!), erzeugt neuen Element-/Fiber-Baum
Commit-Phase
React schreibt Änderungen ins DOM, setzt refs
useLayoutEffect
läuft synchron, noch VOR dem Browser-Paint (blockierend)
Browser malt Bildschirm
useEffect
läuft asynchron NACH dem Paint (nicht blockierend)
Update-Zyklus
State-/Prop-Änderung → Cleanup-Funktion des vorherigen Effects (falls Dependencies sich geändert haben) → erneute Render-Phase → Commit-Phase → neuer Effect
Unmount
Komponente wird aus dem Baum entfernt → Cleanup-Funktionen aller aktiven Effects laufen ein letztes Mal

Virtuelles DOM & Reconciliation

In der Render-Phase erzeugt React keinen echten DOM, sondern einen leichtgewichtigen Baum aus einfachen JavaScript-Objekten – das sogenannte virtuelle DOM (genauer: einen Element-/Fiber-Baum). Bei jedem Render entsteht ein komplett neuer solcher Baum, unabhängig davon, wie klein die eigentliche Änderung war. React vergleicht diesen neuen Baum danach mit dem zuletzt committeten Baum (dieser Diff-Vorgang heißt Reconciliation) und berechnet daraus die minimale Menge an echten DOM-Operationen, die nötig ist, um den Bildschirm auf den neuen Stand zu bringen. Erst diese berechneten Änderungen werden in der Commit-Phase tatsächlich angewendet – nie der komplette Baum am Stück neu aufgebaut.

Der Grund für diesen Umweg: Zwei leichtgewichtige JS-Objektbäume zu vergleichen ist sehr günstig, direkte DOM-Operationen (Reflow, Repaint) sind dagegen teuer. Indem React erst "im Kopf" (virtuell) vergleicht und danach gezielt nur die tatsächlichen Unterschiede ins echte DOM schreibt, bleiben Updates auch bei großen UIs performant.

Woran erkennt React, was sich geändert hat? Beim Vergleich zweier Kindlisten orientiert sich React an der key-Prop (siehe Abschnitt 8): gleicher key an gleicher Position → React geht von "demselben" Element aus und aktualisiert nur die geänderten Attribute; unterschiedlicher/fehlender key (oder Änderung des Element-Typs, z. B. divspan) → React entfernt den alten DOM-Knoten samt internem State (Fokus, Scroll-Position, Eingaben) und erstellt einen neuen.

Warum muss die Render-Phase rein (frei von Seiteneffekten) sein?

Weil die Render-Phase "nur" einen virtuellen Baum berechnet und noch nichts committet, kann React sie jederzeit pausieren, verwerfen oder mehrfach ausführen (Concurrent Rendering), ohne dass am Bildschirm etwas sichtbar wird. Der Funktionskörper einer Komponente darf deshalb während dieser Phase keine sichtbaren Seiteneffekte auslösen (keine DOM-Manipulation, keine Netzwerkaufrufe, kein Mutieren von externem State) – sonst würden Effekte im schlimmsten Fall doppelt oder gar nicht ausgeführt. Seiteneffekte gehören ausschließlich in useEffect/useLayoutEffect (die erst nach dem Commit laufen) oder in Event-Handler.

Timing.jsx – Reihenfolge von State, Effects und Layout-Effects
import { useState, useEffect, useLayoutEffect, useRef } from "react";

function Timing() {
  const [count, setCount] = useState(0);
  const renderCountRef = useRef(0);

  renderCountRef.current++;
  console.log("1) Render-Phase – Funktionskörper läuft, count =", count);

  useLayoutEffect(() => {
    console.log("2) useLayoutEffect – läuft synchron VOR dem Paint");
    return () => console.log("   Cleanup von useLayoutEffect (vor nächstem Layout-Effect / beim Unmount)");
  }, [count]);

  useEffect(() => {
    console.log("3) useEffect – läuft NACH dem Paint (asynchron)");
    return () => console.log("   Cleanup von useEffect (vor nächstem Effect / beim Unmount)");
  }, [count]);

  return (
    <button onClick={() => setCount((c) => c + 1)}>
      Klicks: {count}
    </button>
  );
}
export default Timing;
useEffect vs. useLayoutEffect – Faustregel Nimm useEffect für alles, was nicht sofort sichtbar sein muss (Datenabruf, Logging, Subscriptions, Timer). Nimm useLayoutEffect nur, wenn du das DOM lesen/messen musst (z. B. getBoundingClientRect) und danach synchron etwas anpassen willst, bevor der Browser malt – sonst gibt es ein sichtbares Flackern.
Strict Mode & Concurrent Rendering – doppeltes Rendern in Dev In <React.StrictMode> (nur im Development-Build) führt React 18/19 bei jeder Komponente absichtlich Render-Funktion und Effects doppelt aus – jeweils Mount, Unmount, erneutes Mount (inkl. Cleanup-Aufruf dazwischen). Das deckt unsaubere Effects auf (fehlendes Cleanup, Seiteneffekte im Render). Im Production-Build passiert das nicht. Aus demselben Grund kann die Render-Phase durch Concurrent Features (useTransition, Suspense) unterbrochen und verworfen werden – ein weiterer Grund, warum sie zwingend seiteneffektfrei sein muss.

2. Die wichtigsten Hooks im Überblick

Hooks sind der zentrale Mechanismus, um State, Seiteneffekte und wiederverwendbare Logik in Funktionskomponenten einzubinden. Sie werden immer auf oberster Ebene der Komponente aufgerufen (nicht in Schleifen/Bedingungen).

Kern-Hooks von React – Zweck und typischer Einsatzfall
HookZweckTypischer Einsatzfall
useStateLokaler, primitiver State innerhalb einer KomponenteFormularfeld, Zähler, Toggle-Zustand
useEffectSeiteneffekt nach dem Rendern/Commit ausführen (asynchron, nach Paint)Datenabruf, Subscriptions, Timer, Logging
useLayoutEffectSeiteneffekt synchron vor dem Browser-Paint ausführenDOM messen und Layout korrigieren, Tooltip-Positionierung, Scroll-Restauration
useContextWert aus einem Context ohne Prop-Drilling lesenTheme, angemeldeter User, Sprache/i18n
useMemoErgebnis einer teuren Berechnung zwischenspeichern, bis sich Dependencies ändernGroße Listen filtern/sortieren, abgeleitete Werte
useCallbackFunktionsreferenz stabil halten, bis sich Dependencies ändernCallback an memoisierte Kindkomponente (React.memo) übergeben
useRefVeränderlicher Wert, der KEIN Re-Render auslöst; auch DOM-Knoten-ReferenzFokus setzen, Timer-ID merken, vorherigen Wert speichern
useReducerKomplexere State-Übergänge über Actions/Reducer-Funktion steuernMehrstufige Formulare, State-Maschinen, viele zusammenhängende Felder
useTransitionState-Update als "niedrige Priorität" markieren, UI bleibt währenddessen reaktionsfähigGroße Listen/Suchergebnisse filtern, ohne Tastatureingabe zu blockieren
Custom Hook (useXyz)Wiederverwendbare, zustandsbehaftete Logik aus mehreren Basis-Hooks kapselnuseDebounce, useFetch, useLocalStorage (siehe Abschnitt 13)
Hinweis zu React 19 React 19 ergänzt u. a. useActionState, useOptimistic und den generischen use()-Aufruf (z. B. um Promises/Context bedingt zu lesen) sowie Actions für Formulare. Diese Referenz konzentriert sich bewusst auf die etablierten Kern-Hooks; React 19-spezifische Neuerungen sind am Ende kurz erwähnt.

useState – lokaler State

Liefert einen Wert und eine Setter-Funktion; ein Aufruf des Setters löst einen Re-Render aus. Der Funktions-Updater (c => c + 1) ist sicherer als count + 1, weil er immer auf dem jeweils aktuellsten Wert aufsetzt, auch wenn mehrere Updates kurz hintereinander erfolgen.

useState
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount((c) => c + 1)}>
      Wert: {count}
    </button>
  );
}

useEffect – Seiteneffekt nach dem Commit

Läuft nach dem Browser-Paint, immer wenn sich eine der angegebenen Dependencies seit dem letzten Render geändert hat (siehe auch Abschnitt 19).

useEffect
function DocumentTitle({ unreadCount }) {
  useEffect(() => {
    document.title = unreadCount > 0 ? `(${unreadCount}) Postfach` : "Postfach";
  }, [unreadCount]);

  return <p>Ungelesen: {unreadCount}</p>;
}

useLayoutEffect – synchron vor dem Paint

Wie useEffect, aber blockierend und vor dem Browser-Paint – nötig, wenn eine DOM-Messung sofort in eine Layout-Anpassung einfließen muss, um Flackern zu vermeiden.

useLayoutEffect
function Tooltip({ text }) {
  const ref = useRef(null);
  const [offsetTop, setOffsetTop] = useState(0);

  useLayoutEffect(() => {
    const { height } = ref.current.getBoundingClientRect();
    setOffsetTop(-(height + 8)); // Position VOR dem Paint korrigieren
  }, [text]);

  return (
    <div ref={ref} style={{ position: "absolute", top: offsetTop }}>
      {text}
    </div>
  );
}

useContext – Wert aus einem Context lesen

Liest den aktuellen Wert des nächstgelegenen Provider im Baum, ohne dass er als Prop durchgereicht werden muss (siehe auch Abschnitt 4).

useContext
const ThemeContext = createContext("light");

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button className={`btn btn-${theme}`}>Klick mich</button>;
}

useMemo – teure Berechnung zwischenspeichern

Berechnet den Wert nur dann neu, wenn sich eine der Dependencies geändert hat; sonst wird das zwischengespeicherte Ergebnis aus dem letzten Render zurückgegeben.

useMemo
function ProductList({ products, query }) {
  const filtered = useMemo(
    () => products.filter((p) => p.name.toLowerCase().includes(query.toLowerCase())),
    [products, query]
  );

  return (
    <ul>
      {filtered.map((p) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}

useCallback – stabile Funktionsreferenz

Verhindert, dass bei jedem Render eine neue Funktionsinstanz entsteht – wichtig, wenn die Funktion als Prop an eine mit React.memo memoisierte Kindkomponente übergeben wird und unnötige Re-Renders vermieden werden sollen.

useCallback
const SubmitButton = React.memo(function SubmitButton({ onClick }) {
  console.log("SubmitButton rendert");
  return <button onClick={onClick}>Absenden</button>;
});

function Form() {
  const [text, setText] = useState("");

  // Ohne useCallback würde bei jedem Tastendruck eine neue Funktion
  // entstehen und SubmitButton dadurch trotz React.memo neu rendern.
  const handleSubmit = useCallback(() => {
    console.log("gesendet");
  }, []);

  return (
    <>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <SubmitButton onClick={handleSubmit} />
    </>
  );
}

useRef – veränderlicher Wert ohne Re-Render

Der Rückgabewert (.current) kann beliebig verändert werden, ohne dass ein Re-Render ausgelöst wird. Zwei Haupteinsatzfälle: Zugriff auf einen echten DOM-Knoten, oder das Zwischenspeichern eines Werts über Render-Zyklen hinweg (z. B. eine Timer-ID).

useRef
function AutoFocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus(); // direkter DOM-Zugriff, kein Re-Render nötig
  }, []);

  return <input ref={inputRef} placeholder="Fokus liegt automatisch hier" />;
}

useReducer – komplexe State-Übergänge

Bündelt State-Änderungen in einer reinen Reducer-Funktion, die aus dem bisherigen State und einer Action den neuen State berechnet – übersichtlicher als viele einzelne useState-Aufrufe, sobald mehrere Felder zusammenhängen.

useReducer
function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    case "reset":
      return { count: 0 };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  return (
    <>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
      <span>{state.count}</span>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
    </>
  );
}

useTransition – niedrige Priorität für State-Updates

Markiert ein State-Update als unterbrechbar/nicht dringend, sodass dringende Updates (z. B. Tastatureingabe) währenddessen weiterhin sofort verarbeitet werden. isPending zeigt an, ob das niedrig priorisierte Update noch läuft.

useTransition
function SearchableList({ items }) {
  const [query, setQuery] = useState("");
  const [filtered, setFiltered] = useState(items);
  const [isPending, startTransition] = useTransition();

  function handleChange(event) {
    const value = event.target.value;
    setQuery(value); // dringend: sofort im Input anzeigen

    startTransition(() => {
      // niedrige Priorität: darf verzögert/unterbrochen werden
      setFiltered(items.filter((i) => i.includes(value)));
    });
  }

  return (
    <>
      <input value={query} onChange={handleChange} />
      {isPending && <span>Aktualisiere Liste…</span>}
      <ul>
        {filtered.map((i) => (
          <li key={i}>{i}</li>
        ))}
      </ul>
    </>
  );
}

Custom Hooks – eigene Logik kapseln

Jede Funktion, deren Name mit use beginnt und die intern andere Hooks aufruft, ist ein Custom Hook. So lässt sich zustandsbehaftete Logik zwischen Komponenten teilen, ohne HOCs oder Render Props.

Custom Hook: useToggle
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);
  const toggle = useCallback(() => setValue((v) => !v), []);
  return [value, toggle];
}

// Verwendung:
function Accordion() {
  const [isOpen, toggleOpen] = useToggle(false);
  return (
    <>
      <button onClick={toggleOpen}>{isOpen ? "Schließen" : "Öffnen"}</button>
      {isOpen && <p>Inhalt…</p>}
    </>
  );
}

Ein vollständigeres Beispiel (useDebounce, inkl. Cleanup) findest du in Abschnitt 13.

3. Component-Composition-Patterns

React bietet mehrere Muster, um Komponenten flexibel zusammenzusetzen, statt Verhalten über tiefe Vererbungshierarchien zu modellieren.

PatternIdeeWann sinnvoll
Children-PropsBeliebiger JSX-Inhalt wird über props.children "durchgereicht"Layout-Wrapper, Card, Modal-Hülle
Compound ComponentsMehrere Komponenten teilen sich implizit State über Context, werden aber als zusammengehörige "Familie" verwendet (<Tabs><Tabs.Tab/></Tabs>)Tabs, Accordion, Select/Menu mit flexiblem Markup
Render PropsEine Prop ist eine Funktion, die JSX zurückgibt; die Komponente ruft sie mit internem State aufWiederverwendbare Logik, die dem Aufrufer die Darstellung überlässt (heute oft durch Custom Hooks ersetzt)
Higher-Order Components (HOC)Funktion, die eine Komponente entgegennimmt und eine neue, "angereicherte" Komponente zurückgibtCross-Cutting Concerns wie withAuth(Component), withLogging(Component) (heute meist durch Hooks ersetzt)

Beispiel: Compound Components (Tabs)

Tabs.jsx
import { createContext, useContext, useState } from "react";

const TabsContext = createContext(null);

function Tabs({ defaultValue, children }) {
  const [active, setActive] = useState(defaultValue);
  return (
    <TabsContext.Provider value={{ active, setActive }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
}

function TabList({ children }) {
  return <div className="tab-list" role="tablist">{children}</div>;
}

function Tab({ value, children }) {
  const { active, setActive } = useContext(TabsContext);
  return (
    <button
      role="tab"
      aria-selected={active === value}
      onClick={() => setActive(value)}
    >
      {children}
    </button>
  );
}

function TabPanel({ value, children }) {
  const { active } = useContext(TabsContext);
  if (active !== value) return null;
  return <div role="tabpanel">{children}</div>;
}

// "Zusammengesetzte" API: Tabs.List, Tabs.Tab, Tabs.Panel
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;

export default Tabs;

// Verwendung:
// <Tabs defaultValue="general">
//   <Tabs.List>
//     <Tabs.Tab value="general">Allgemein</Tabs.Tab>
//     <Tabs.Tab value="security">Sicherheit</Tabs.Tab>
//   </Tabs.List>
//   <Tabs.Panel value="general">Inhalt Allgemein</Tabs.Panel>
//   <Tabs.Panel value="security">Inhalt Sicherheit</Tabs.Panel>
// </Tabs>

Der große Vorteil: Der Aufrufer bestimmt Reihenfolge und Markup-Struktur frei, während Tabs intern den State (welcher Tab aktiv ist) verwaltet und über Context an alle Kinder verteilt – ohne dass Props manuell durchgereicht werden müssen.

4. State-Management-Ebenen

React kennt keine eingebauten "Scopes" wie serverseitige Frameworks – stattdessen wählt man bewusst die "Reichweite", in der ein Stück State lebt.

EbeneAPILebensdauer / SichtbarkeitTypischer Einsatz
Lokaler StateuseState / useReducerLebt nur innerhalb einer Komponenteninstanz, verschwindet beim UnmountFormularfeld, Toggle, Zähler, UI-State
ContextcreateContext / useContextLebt so lange wie der Provider im Baum gemountet ist; sichtbar für alle Nachfahren des ProvidersTheme, angemeldeter User, Sprache – Daten, die "quer" durch viele Komponenten gebraucht werden
Externer StoreZustand, Redux Toolkit, Jotai u. Ä.Lebt außerhalb des Komponentenbaums (Modul-Singleton); überlebt Mount/Unmount einzelner Komponenten, i. d. R. für die gesamte App-SessionWarenkorb, globaler App-State, komplexe/häufig aktualisierte Daten, die viele nicht verwandte Komponenten lesen

Lokaler State

Counter.jsx
import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount((c) => c + 1)}>
      Wert: {count}
    </button>
  );
}
export default Counter;

Context API

ThemeContext.jsx
import { createContext, useContext, useState } from "react";

const ThemeContext = createContext("light");

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  return useContext(ThemeContext);
}

// Verwendung in einer beliebigen Nachfahren-Komponente:
function ThemeToggle() {
  const { theme, setTheme } = useTheme();
  return (
    <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
      Aktuelles Theme: {theme}
    </button>
  );
}

Globaler State mit Zustand

cartStore.js
import { create } from "zustand";

export const useCartStore = create((set) => ({
  items: [],
  addItem: (item) =>
    set((state) => ({ items: [...state.items, item] })),
  clear: () => set({ items: [] }),
}));

// Verwendung in JEDER Komponente, ganz ohne Provider-Wrapper:
function CartBadge() {
  const itemCount = useCartStore((state) => state.items.length);
  return <span className="badge">{itemCount}</span>;
}

5. Vergleich der State-Ansätze

Prop Drilling, Context und externer Store lösen dasselbe Problem ("wie kommt ein Wert zu einer entfernten Komponente") mit unterschiedlichen Kompromissen.

AnsatzVorteileNachteileWann sinnvoll
Prop Drilling Explizit, einfach nachzuvollziehen, keine zusätzliche Abhängigkeit Wird bei tiefen Baumstrukturen unübersichtlich; Zwischenkomponenten müssen Props durchreichen, die sie selbst nicht brauchen Flache Komponentenbäume, wenige Ebenen (2–3)
Context API Kein manuelles Durchreichen mehr nötig; in React eingebaut Jede Änderung des Context-Werts rendert alle Consumer neu, unabhängig davon, welchen Teil des Werts sie tatsächlich nutzen; bei häufig wechselnden Werten (z. B. Formulareingaben) drohen Re-Render-Kaskaden über den ganzen Unterbaum Selten wechselnde, "globale" Werte (Theme, Auth-User, Sprache), moderate Update-Frequenz
Externer Store (Zustand/Redux) Selektiver Re-Render nur für Komponenten, die den konkret geänderten Ausschnitt lesen (via Selector); State lebt unabhängig vom Komponentenbaum; DevTools/Middleware verfügbar Zusätzliche Abhängigkeit, mehr Boilerplate/Lernaufwand, potenzielle "Magie" bei komplexen Selektoren Häufig aktualisierter, app-weiter State mit vielen, nicht direkt verwandten Konsumenten (Warenkorb, Benachrichtigungen, komplexe Formulardaten über mehrere Routen hinweg)
Nachteile von zu viel Context Ein einziger großer "App-Context" mit vielen Feldern führt dazu, dass jede noch so kleine Änderung (z. B. ein Tastendruck in einem Suchfeld) alle Consumer neu rendert – selbst wenn diese ganz andere Werte aus dem Context nutzen. Gegenmittel: Context in mehrere, fachlich getrennte Provider aufteilen, Werte mit useMemo stabilisieren, oder für häufig wechselnde Daten direkt einen externen Store mit Selector-Unterstützung verwenden.

6. Navigation/Routing mit React Router

React selbst enthält kein Routing – react-router-dom (aktuell v6/v7) ist der De-facto-Standard für clientseitiges Routing in SPAs.

App.jsx – Routen-Definition, verschachtelte Routen
import { BrowserRouter, Routes, Route, Link, Outlet, useNavigate } from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="users" element={<Users />}>
            <Route path=":userId" element={<UserDetail />} />
          </Route>
          <Route path="contact" element={<ContactForm />} />
          <Route path="*" element={<NotFound />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

function Layout() {
  return (
    <div>
      <nav>
        <Link to="/">Start</Link>
        <Link to="/users">Benutzer</Link>
        <Link to="/contact">Kontakt</Link>
      </nav>
      {/* Outlet rendert die jeweils passende Kind-Route (verschachtelte Routen) */}
      <Outlet />
    </div>
  );
}

function ContactForm() {
  const navigate = useNavigate();

  function handleSubmit(event) {
    event.preventDefault();
    // ... Formular absenden ...
    navigate("/"); // Redirect nach erfolgreichem Submit
  }

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit">Absenden</button>
    </form>
  );
}

export default App;

Mit useParams() liest UserDetail die :userId aus der URL (siehe auch Abschnitt 15). Für deklarative Redirects ohne Event (z. B. Schutz einer Route) eignet sich das <Navigate to="/login" />-Element.

7. Fehlerbehandlung

Error Boundaries

Error Boundaries fangen Rendering-Fehler in ihrem Kindbaum ab und zeigen einen Fallback, statt dass die ganze App weiß wird. Sie müssen aktuell als Klassenkomponente implementiert werden, weil React dafür die Lifecycle-Methoden static getDerivedStateFromError und componentDidCatch benötigt – ein Hook-Äquivalent existiert (Stand React 19) nicht, da Hooks keine Fehler aus dem Rendering von Kindkomponenten abfangen können. In der Praxis nutzt man daher meist die fertige Bibliothek react-error-boundary, die diese Klasse kapselt und eine funktionale API bietet.

ErrorBoundary.jsx – einzige nötige Klassenkomponente
import { Component } from "react";

class ErrorBoundary extends Component {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    console.error("Unerwarteter Fehler:", error, info);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback ?? <p>Etwas ist schiefgelaufen.</p>;
    }
    return this.props.children;
  }
}

export default ErrorBoundary;

// Verwendung:
// <ErrorBoundary fallback={<p>Diese Ansicht ist leider abgestürzt.</p>}>
//   <Dashboard />
// </ErrorBoundary>
Alternative: react-error-boundary npm i react-error-boundary liefert <ErrorBoundary FallbackComponent={...} onReset={...}> als fertige Komponente – man muss die Klasse selbst nie schreiben, sondern konsumiert sie nur funktional.

try/catch in async Event-Handlern

Error Boundaries fangen keine Fehler aus Event-Handlern oder asynchronem Code ab. Dafür ist ganz normales try/catch zuständig.

SaveButton.jsx
function SaveButton({ data }) {
  async function handleClick() {
    try {
      const response = await fetch("/api/save", {
        method: "POST",
        body: JSON.stringify(data),
        headers: { "Content-Type": "application/json" },
      });
      if (!response.ok) throw new Error("Speichern fehlgeschlagen: " + response.status);
      alert("Gespeichert!");
    } catch (error) {
      console.error(error);
      alert("Fehler beim Speichern: " + error.message);
    }
  }

  return <button onClick={handleClick}>Speichern</button>;
}

Fehlerbehandlung beim Datenfetching

Bei reinem fetch in useEffect muss der Fehlerzustand manuell in einem eigenen State gehalten werden. Bibliotheken wie React Query (@tanstack/react-query) übernehmen das inklusive Retry-Logik.

UserProfile.jsx – mit React Query
import { useQuery } from "@tanstack/react-query";

function UserProfile({ userId }) {
  const { data, isLoading, isError, error } = useQuery({
    queryKey: ["user", userId],
    queryFn: () =>
      fetch(`/api/users/${userId}`).then((res) => {
        if (!res.ok) throw new Error("Nutzer nicht gefunden");
        return res.json();
      }),
  });

  if (isLoading) return <p>Lädt…</p>;
  if (isError) return <p>Fehler: {error.message}</p>;
  return <p>{data.name}</p>;
}

8. JSX & Ausdrücke

JSX ist eine Syntaxerweiterung von JavaScript, die HTML-ähnliches Markup direkt im Code erlaubt. Ein Build-Tool (Babel/SWC) übersetzt JSX in reine React.createElement(...)-Aufrufe – im Browser läuft am Ende nur JavaScript.

KonstruktBeispielBedeutung
Expression {}<p>{user.name}</p>Beliebiger JS-Ausdruck wird ausgewertet und eingefügt
Bedingung (&&){isAdmin && <AdminPanel />}Element nur rendern, wenn Bedingung true ist
Bedingung (Ternary){isLoading ? <Spinner /> : <Content />}Eines von zwei Elementen rendern
Listen-Rendering{items.map((i) => <li key={i.id}>{i.label}</li>)}Array in Elemente umwandeln; key muss stabil und eindeutig sein (nicht der Array-Index, wenn sich die Reihenfolge ändern kann)
Fragment<><Header /><Body /></>Mehrere Elemente zurückgeben, ohne ein zusätzliches DOM-Element zu erzeugen
JsxBasics.jsx
function UserCard({ user, isAdmin, permissions }) {
  return (
    <>
      <h3>{user.name}</h3>

      {isAdmin && <span className="badge">Admin</span>}

      {user.active ? (
        <span className="status ok">Aktiv</span>
      ) : (
        <span className="status inactive">Inaktiv</span>
      )}

      <ul>
        {permissions.map((p) => (
          <li key={p.id}>{p.label}</li>
        ))}
      </ul>
    </>
  );
}
Häufiger Fehler bei && {count && <Badge />} rendert bei count === 0 eine sichtbare "0" statt nichts, weil 0 ein falsy, aber renderbarer Wert ist. Besser: {count > 0 && <Badge />} oder Ternary verwenden.

9. Datenabruf ("Ajax"-Äquivalent)

Der klassische Weg ist fetch/axios innerhalb eines useEffect, inklusive sauberem Abbruch laufender Requests beim Unmount oder bei geänderten Dependencies.

UserList.jsx – fetch mit AbortController
import { useEffect, useState } from "react";

function UserList({ searchTerm }) {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    async function loadUsers() {
      setLoading(true);
      setError(null);
      try {
        const res = await fetch(`/api/users?q=${searchTerm}`, {
          signal: controller.signal,
        });
        if (!res.ok) throw new Error("Serverfehler: " + res.status);
        const data = await res.json();
        setUsers(data);
      } catch (err) {
        if (err.name !== "AbortError") setError(err);
      } finally {
        setLoading(false);
      }
    }

    loadUsers();

    // Cleanup: laufenden Request abbrechen, wenn sich searchTerm ändert
    // oder die Komponente unmountet
    return () => controller.abort();
  }, [searchTerm]);

  if (loading) return <p>Lädt…</p>;
  if (error) return <p>Fehler: {error.message}</p>;
  return (
    <ul>
      {users.map((u) => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  );
}
Alternative: React Query / SWR Für alles, was über einen einzelnen einfachen Abruf hinausgeht, lohnen sich @tanstack/react-query oder swr: sie übernehmen Caching, automatisches Neuladen bei Fenster-Fokus, Deduplizierung paralleler Requests, Retry-Logik sowie Lade-/Fehlerzustände – man schreibt nur noch useQuery({ queryKey, queryFn }) statt useEffect + drei useState-Aufrufe manuell zu pflegen.

10. Ein durchgängiges Beispiel

Layout-Komponente, eine Route mit Formular, Eingabe, Submit-Handler und Anzeige des Ergebnis-States – der komplette Flow von Route bis Anzeige.

FeedbackPage.jsx
import { useState } from "react";
import { Outlet, Link } from "react-router-dom";

// --- Layout ---
function AppLayout() {
  return (
    <div className="app">
      <header>
        <Link to="/feedback">Feedback</Link>
      </header>
      <main>
        <Outlet />
      </main>
    </div>
  );
}

// --- Route mit Formular ---
function FeedbackPage() {
  const [message, setMessage] = useState("");
  const [submittedList, setSubmittedList] = useState([]);
  const [status, setStatus] = useState("idle"); // idle | sending | done | error

  async function handleSubmit(event) {
    event.preventDefault();
    if (!message.trim()) return;

    setStatus("sending");
    try {
      const res = await fetch("/api/feedback", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ message }),
      });
      if (!res.ok) throw new Error("Senden fehlgeschlagen");

      // State aktualisieren: neue Nachricht in die Liste aufnehmen
      setSubmittedList((prev) => [...prev, message]);
      setMessage("");
      setStatus("done");
    } catch {
      setStatus("error");
    }
  }

  return (
    <section>
      <h1>Feedback</h1>

      <form onSubmit={handleSubmit}>
        <label htmlFor="message">Ihre Nachricht</label>
        <textarea
          id="message"
          value={message}
          onChange={(e) => setMessage(e.target.value)}
        />
        <button type="submit" disabled={status === "sending"}>
          {status === "sending" ? "Wird gesendet…" : "Absenden"}
        </button>
      </form>

      {status === "done" && <p>Danke für Ihr Feedback!</p>}
      {status === "error" && <p>Es ist ein Fehler aufgetreten.</p>}

      <h2>Bisherige Nachrichten in dieser Sitzung</h2>
      <ul>
        {submittedList.map((msg, i) => (
          <li key={i}>{msg}</li>
        ))}
      </ul>
    </section>
  );
}

export { AppLayout, FeedbackPage };

// Einbindung in die Routen (siehe Abschnitt 6):
// <Route path="/" element={<AppLayout />}>
//   <Route path="feedback" element={<FeedbackPage />} />
// </Route>

11. GUI-Elemente ein-/ausblenden

Es gibt zwei grundverschiedene Wege, ein Element sichtbar/unsichtbar zu machen: rein visuell über CSS am bestehenden DOM-Knoten (ohne Re-Render), oder über bedingtes Rendern mit State (mit Re-Render, DOM-Knoten wird entfernt/neu erzeugt).

Variante A: CSS-Klasse toggeln über ref (kein State, kein Re-Render)
import { useRef } from "react";

function CollapsibleNoState({ children }) {
  const panelRef = useRef(null);

  function toggle() {
    // Direkte DOM-Manipulation – React weiß davon nichts, es gibt keinen Re-Render
    panelRef.current.classList.toggle("hidden");
  }

  return (
    <div>
      <button onClick={toggle}>Ein-/Ausblenden</button>
      <div ref={panelRef} className="panel">
        {children}
      </div>
    </div>
  );
}
Variante B: Bedingtes Rendern über useState (mit Re-Render)
import { useState } from "react";

function CollapsibleWithState({ children }) {
  const [open, setOpen] = useState(true);

  return (
    <div>
      <button onClick={() => setOpen((o) => !o)}>Ein-/Ausblenden</button>
      {open && <div className="panel">{children}</div>}
    </div>
  );
}
KriteriumCSS-Klasse via ref (ohne State)Bedingtes Rendern via State
Re-Render ausgelöst?Nein – React-Baum bleibt unverändertJa – Komponente (und ggf. Kinder) rendert neu
DOM-Knoten / interner State des KindbaumsBleibt erhalten (z. B. Scroll-Position, Eingabewerte, Video-Wiedergabeposition)Geht verloren, sobald der Knoten aus dem Baum entfernt wird (bei erneutem Einblenden: Neuinitialisierung, Effects laufen erneut)
Performance bei sehr häufigem UmschaltenSehr günstig (reine CSS-Operation)Etwas teurer (React-Reconciliation), in der Praxis meist vernachlässigbar
Zugänglichkeit von verstecktem InhaltInhalt existiert weiterhin im DOM (ggf. aria-hidden nötig)Inhalt existiert nicht im DOM, wenn ausgeblendet (sauberer für Screenreader)
Wann sinnvollRein optische Zustände, hochfrequentes Umschalten, Animationen, wenn interner State erhalten bleiben sollWenn das Element wirklich weg sein soll (auch aus Barrierefreiheits-/SEO-Sicht) oder wenn Sichtbarkeit an fachlichen State gekoppelt ist

12. Formular-Validierung mit react-hook-form + Zod

react-hook-form verwaltet Formularzustand performant (unkontrollierte Inputs, minimale Re-Renders), zod definiert deklarative Validierungsregeln, die per @hookform/resolvers/zod eingebunden werden.

RegisterForm.jsx
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

// Validierungsregeln, analog zu Bean-Validation-Annotationen (@NotNull, @Email, @Size ...)
const schema = z.object({
  username: z.string().min(3, "Mindestens 3 Zeichen").max(20, "Maximal 20 Zeichen"),
  email: z.string().email("Ungültige E-Mail-Adresse"),
  age: z
    .number({ invalid_type_error: "Bitte eine Zahl eingeben" })
    .int()
    .min(18, "Mindestalter ist 18"),
});

function RegisterForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm({
    resolver: zodResolver(schema),
    defaultValues: { username: "", email: "", age: 18 },
  });

  async function onValid(data) {
    await fetch("/api/register", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(data),
    });
  }

  return (
    <form onSubmit={handleSubmit(onValid)}>
      <label>
        Benutzername
        <input {...register("username")} />
      </label>
      {errors.username && <p className="error">{errors.username.message}</p>}

      <label>
        E-Mail
        <input type="email" {...register("email")} />
      </label>
      {errors.email && <p className="error">{errors.email.message}</p>}

      <label>
        Alter
        <input type="number" {...register("age", { valueAsNumber: true })} />
      </label>
      {errors.age && <p className="error">{errors.age.message}</p>}

      <button type="submit" disabled={isSubmitting}>Registrieren</button>
    </form>
  );
}

export default RegisterForm;

Vorteil dieser Kombination: Das Zod-Schema ist die "single source of truth" für Validierungsregeln und kann – anders als reine Client-Validierung – identisch auch serverseitig (z. B. in einer Node/Express- oder Next.js-Route) wiederverwendet werden.

13. Custom Hook als Beispiel für Wiederverwendbarkeit

Ein Custom Hook ist einfach eine Funktion, deren Name mit use beginnt und die intern andere Hooks aufruft. So lässt sich zustandsbehaftete Logik zwischen Komponenten teilen, ohne HOCs oder Render Props.

useDebounce.js
import { useEffect, useState } from "react";

/**
 * Verzögert die Übernahme eines Werts um `delay` Millisekunden.
 * Nützlich z. B. für Live-Suchfelder, um nicht bei jedem Tastendruck
 * einen Request abzusetzen.
 */
function useDebounce(value, delay = 300) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    // Cleanup: alten Timer verwerfen, wenn sich value/delay
    // erneut ändern, bevor die Zeit abgelaufen ist
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

export default useDebounce;
SearchBox.jsx – Verwendung
import { useState, useEffect } from "react";
import useDebounce from "./useDebounce";

function SearchBox() {
  const [input, setInput] = useState("");
  const debouncedInput = useDebounce(input, 400);

  useEffect(() => {
    if (!debouncedInput) return;
    fetch(`/api/search?q=${debouncedInput}`);
    // ... Ergebnis verarbeiten ...
  }, [debouncedInput]);

  return (
    <input
      value={input}
      onChange={(e) => setInput(e.target.value)}
      placeholder="Suchen…"
    />
  );
}

14. File Upload

Dateien werden über <input type="file"> ausgewählt, im Change-Handler aus event.target.files gelesen und typischerweise per FormData + fetch hochgeladen.

FileUpload.jsx
import { useState } from "react";

function FileUpload() {
  const [file, setFile] = useState(null);
  const [status, setStatus] = useState("idle");

  function handleFileChange(event) {
    const selected = event.target.files?.[0] ?? null;
    setFile(selected);
  }

  async function handleUpload() {
    if (!file) return;

    const formData = new FormData();
    formData.append("file", file);

    setStatus("uploading");
    try {
      const res = await fetch("/api/upload", {
        method: "POST",
        body: formData, // Content-Type NICHT manuell setzen – der Browser
                         // ergänzt automatisch den korrekten multipart-Boundary-Header
      });
      if (!res.ok) throw new Error("Upload fehlgeschlagen");
      setStatus("done");
    } catch (err) {
      console.error(err);
      setStatus("error");
    }
  }

  return (
    <div>
      <input type="file" accept="image/*" onChange={handleFileChange} />
      {file && <p>Ausgewählt: {file.name} ({Math.round(file.size / 1024)} KB)</p>}
      <button onClick={handleUpload} disabled={!file || status === "uploading"}>
        Hochladen
      </button>
      {status === "done" && <p>Upload erfolgreich!</p>}
      {status === "error" && <p>Fehler beim Upload.</p>}
    </div>
  );
}

export default FileUpload;

15. URL-Query-Parameter & Bookmarkbarkeit

Damit eine Ansicht per URL teilbar/bookmarkbar ist, gehört ihr Zustand (z. B. eine ID oder ein Filter) in die URL – nicht nur in internen State. React Router stellt dafür useParams (Pfad-Segmente) und useSearchParams (Query-String) bereit.

ProductDetail.jsx – ID im Pfad, Filter im Query-String
import { useParams, useSearchParams, Link } from "react-router-dom";

// Route: <Route path="/products/:productId" element={<ProductDetail />} />
// Aufgerufene URL z. B.: /products/42?tab=reviews

function ProductDetail() {
  const { productId } = useParams();
  const [searchParams, setSearchParams] = useSearchParams();
  const activeTab = searchParams.get("tab") ?? "info";

  function selectTab(tab) {
    setSearchParams({ tab }); // aktualisiert die URL, ohne Seite neu zu laden
  }

  return (
    <div>
      <h1>Produkt #{productId}</h1>

      <nav>
        <button onClick={() => selectTab("info")}>Info</button>
        <button onClick={() => selectTab("reviews")}>Bewertungen</button>
      </nav>

      {activeTab === "info" && <p>Produktinformationen zu {productId}…</p>}
      {activeTab === "reviews" && <p>Bewertungen zu {productId}…</p>}

      <Link to={`/products/${productId}?tab=reviews`}>
        Direktlink zu den Bewertungen
      </Link>
    </div>
  );
}

export default ProductDetail;

Weil productId und tab Teil der URL sind, funktionieren Browser-Zurück/Vorwärts, Lesezeichen und geteilte Links korrekt – ein erneuter Aufruf der URL stellt exakt denselben Zustand wieder her.

16. Asset- & Resource-Handling

Styling: CSS-Module vs. styled-components

CSS-Module (Button.module.css) erzeugen beim Build automatisch eindeutige Klassennamen, sodass Styles nicht versehentlich kollidieren – normales CSS bleibt aber die Syntax. styled-components (bzw. Alternativen wie Emotion) definieren Styles direkt als JavaScript-Template-Literale, gekoppelt an eine Komponente, inklusive Props-basiertem dynamischem Styling.

CSS-Module
// Button.module.css
// .primary { background: royalblue; color: white; }

import styles from "./Button.module.css";

function Button({ children }) {
  return <button className={styles.primary}>{children}</button>;
}
styled-components
import styled from "styled-components";

const PrimaryButton = styled.button`
  background: ${(props) => (props.$danger ? "crimson" : "royalblue")};
  color: white;
  padding: 8px 16px;
`;

function Button({ children, danger }) {
  return <PrimaryButton $danger={danger}>{children}</PrimaryButton>;
}

Bild-Imports

Logo.jsx
import logoUrl from "./assets/logo.png"; // Build-Tool liefert eine (ggf. gehashte) URL

function Header() {
  return <img src={logoUrl} alt="Firmenlogo" width={120} />;
}

Code-Splitting mit React.lazy + Suspense

Große, selten benötigte Komponenten (z. B. ein Admin-Bereich) lassen sich per dynamischem Import erst laden, wenn sie tatsächlich gebraucht werden – das verkleinert das initiale JS-Bundle.

App.jsx
import { lazy, Suspense } from "react";

const AdminPanel = lazy(() => import("./AdminPanel.jsx"));

function App() {
  return (
    <Suspense fallback={<p>Lädt Admin-Bereich…</p>}>
      <AdminPanel />
    </Suspense>
  );
}

17. Internationalisierung mit react-i18next

react-i18next lädt Übersetzungen aus JSON-Dateien pro Sprache und stellt über den useTranslation-Hook eine t()-Funktion sowie das Umschalten der aktiven Sprache bereit.

locales/de/translation.json
{
  "greeting": "Hallo, {{name}}!",
  "nav": { "home": "Start", "contact": "Kontakt" }
}
locales/en/translation.json
{
  "greeting": "Hello, {{name}}!",
  "nav": { "home": "Home", "contact": "Contact" }
}
i18n.js – Grundsetup
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import de from "./locales/de/translation.json";
import en from "./locales/en/translation.json";

i18n.use(initReactI18next).init({
  resources: { de: { translation: de }, en: { translation: en } },
  lng: "de",
  fallbackLng: "en",
  interpolation: { escapeValue: false },
});

export default i18n;
Greeting.jsx – Verwendung inkl. Sprachumschaltung
import { useTranslation } from "react-i18next";

function Greeting({ userName }) {
  const { t, i18n } = useTranslation();

  return (
    <div>
      <p>{t("greeting", { name: userName })}</p>
      <nav>
        <a href="/">{t("nav.home")}</a>
        <a href="/contact">{t("nav.contact")}</a>
      </nav>

      <button onClick={() => i18n.changeLanguage("de")}>DE</button>
      <button onClick={() => i18n.changeLanguage("en")}>EN</button>
    </div>
  );
}

18. State-Persistenz & Sicherheitsaspekte

State in localStorage/sessionStorage spiegeln

Ein einfacher, selbst geschriebener Sync-Effect genügt für viele Fälle; für komplexere globale Stores übernimmt redux-persist (bzw. Zustands eingebaute persist-Middleware) das automatisch inklusive Rehydration beim Start.

usePersistentState.js – eigener useEffect-Sync
import { useState, useEffect } from "react";

function usePersistentState(key, initialValue) {
  const [value, setValue] = useState(() => {
    const stored = window.localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });

  useEffect(() => {
    window.localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

// Verwendung: const [draft, setDraft] = usePersistentState("feedback-draft", "");
Zustand mit persist-Middleware
import { create } from "zustand";
import { persist } from "zustand/middleware";

export const useSettingsStore = create(
  persist(
    (set) => ({
      theme: "light",
      setTheme: (theme) => set({ theme }),
    }),
    { name: "app-settings" } // localStorage-Key
  )
);

CSRF-Handling bei API-Calls

Bei Session-Cookie-basierter Authentifizierung muss ein CSRF-Token bei verändernden Requests (POST/PUT/DELETE) mitgeschickt werden, üblicherweise als Header, dessen Wert zuvor aus einem Cookie oder einem serverseitig gerenderten Meta-Tag gelesen wurde.

apiClient.js
function getCsrfToken() {
  return document
    .querySelector('meta[name="csrf-token"]')
    ?.getAttribute("content");
}

async function apiPost(url, body) {
  return fetch(url, {
    method: "POST",
    credentials: "include", // Session-Cookie mitschicken
    headers: {
      "Content-Type": "application/json",
      "X-CSRF-Token": getCsrfToken(),
    },
    body: JSON.stringify(body),
  });
}

export { apiPost };
Hinweis Sensible Daten (Tokens, personenbezogene Daten) sollten nicht unnötig in localStorage gespiegelt werden, da dieser für jedes im selben Origin laufende JavaScript (auch über XSS) auslesbar ist. Für Auth-Tokens sind httpOnly-Cookies vom Server die sicherere Alternative.

19. Mehrstufige Formulare/Wizards & useEffect als Lifecycle-Hook

Wizard mit useReducer

RegistrationWizard.jsx
import { useReducer } from "react";

const initialState = { step: 1, data: { name: "", email: "", plan: "" } };

function reducer(state, action) {
  switch (action.type) {
    case "NEXT_STEP":
      return { ...state, step: state.step + 1 };
    case "PREV_STEP":
      return { ...state, step: state.step - 1 };
    case "UPDATE_FIELD":
      return { ...state, data: { ...state.data, [action.field]: action.value } };
    default:
      return state;
  }
}

function RegistrationWizard() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      {state.step === 1 && (
        <input
          placeholder="Name"
          value={state.data.name}
          onChange={(e) => dispatch({ type: "UPDATE_FIELD", field: "name", value: e.target.value })}
        />
      )}
      {state.step === 2 && (
        <input
          placeholder="E-Mail"
          value={state.data.email}
          onChange={(e) => dispatch({ type: "UPDATE_FIELD", field: "email", value: e.target.value })}
        />
      )}
      {state.step === 3 && <p>Zusammenfassung: {state.data.name}, {state.data.email}</p>}

      {state.step > 1 && <button onClick={() => dispatch({ type: "PREV_STEP" })}>Zurück</button>}
      {state.step < 3 && <button onClick={() => dispatch({ type: "NEXT_STEP" })}>Weiter</button>}
    </div>
  );
}

useEffect als Lifecycle-Hook: Dependency-Array-Varianten

Dependency-ArrayWann läuft der Effect?Typischer Einsatz
kein Array: useEffect(fn)Nach jedem Render (Mount und jedem Update)Selten sinnvoll, meist ein Zeichen für ein Missverständnis
leeres Array: useEffect(fn, [])Nur einmal, direkt nach dem MountEinmalige Initialisierung, Abo/Subscription aufsetzen
mit Dependencies: useEffect(fn, [a, b])Nach dem Mount, danach immer wenn sich a oder b seit dem letzten Render geändert habenDatenabruf abhängig von einer ID, Reaktion auf Prop-/State-Änderung
Cleanup-Funktion (Rückgabewert)Läuft vor dem nächsten Effect-Durchlauf (bei geänderten Dependencies) sowie ein letztes Mal beim UnmountTimer löschen, Event-Listener entfernen, Requests abbrechen (siehe AbortController in Abschnitt 9)

Effekte sind damit funktional das Gegenstück zu Ereignis-basiertem Reagieren auf Lifecycle-Übergänge: Statt auf benannte Phasenwechsel zu lauschen, deklariert man wovon ein Effekt abhängt – React entscheidet dann selbst, wann er (erneut) ausgeführt werden muss.

ChatRoom.jsx – Effect mit Cleanup bei wechselnder Dependency
import { useEffect, useState } from "react";

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    console.log("Verbinde mit Raum", roomId);
    const connection = createConnection(roomId); // fiktive API
    connection.on("message", (msg) => setMessages((m) => [...m, msg]));
    connection.connect();

    return () => {
      console.log("Trenne Verbindung zu Raum", roomId);
      connection.disconnect();
    };
  }, [roomId]); // Bei Wechsel von roomId: erst alte Verbindung trennen, dann neue aufbauen

  return (
    <ul>
      {messages.map((m, i) => (
        <li key={i}>{m}</li>
      ))}
    </ul>
  );
}

Fazit & offene Punkte

Die obigen 19 Abschnitte decken den React-Alltag ab: Rendering-Modell, Hooks, Composition, State-Ebenen, Routing, Fehlerbehandlung, Formulare, Datenabruf und einige Querschnittsthemen (i18n, Persistenz, Sicherheit).

Ein paar Themen habe ich bewusst nicht vertieft, die für einen vollständigen Überblick perspektivisch noch relevant sein könnten:

ThemaWarum relevant
Server Components (RSC) & Frameworks wie Next.jsVerändert grundlegend, wo Komponenten ausgeführt werden (Server vs. Client) und wie Datenabruf/Bundle-Größe funktionieren – ein eigener, größerer Themenblock
Suspense für Datenfetching (nicht nur Code-Splitting)React 19/Frameworks erlauben use() mit Promises direkt im Rendering statt manuellem Lade-State
Testing (React Testing Library, Vitest/Jest)Ohne automatisierte Tests bleibt jede Referenz nur "Theorie" – Komponenten-, Hook- und Integrationstests wären ein sinnvoller nächster Baustein
Performance-Feintuning (React.memo, Profiler, virtualisierte Listen)Wichtig, sobald Listen/Bäume größer werden
React 19 Actions/useActionState/useOptimisticNeuer, zunehmend empfohlener Ansatz speziell für Formular-Submits mit Server-Interaktion

Sag gern Bescheid, falls einer dieser Punkte (oder ein anderer) noch als eigener Abschnitt ergänzt werden soll.

Hinweis zu Marken- und Produktnamen: In diesem Dokument genannte Produkt-, Firmen- und Markennamen — u. a. React, React Router, Vite, Create React App (CRA), Zustand, Redux Toolkit, Jotai, react-error-boundary, npm, React Query, Babel, SWC, SWR, react-hook-form, Zod, Node.js, Express, Next.js, styled-components, Emotion, react-i18next, i18next, redux-persist, React Testing Library, Vitest, Jest — sind Marken bzw. eingetragene Marken der jeweiligen Rechteinhaber. Sie werden hier ausschließlich zu illustrativen und erklärenden Zwecken verwendet; eine Zugehörigkeit, Empfehlung oder Zusammenarbeit mit den jeweiligen Unternehmen ist damit nicht verbunden.