React – Konzepte im Überblick
Eine systematische Referenz über die zentralen Konzepte von React (funktionale Komponenten & Hooks, React 18/19). Alle Codebeispiele sind bewusst kompakt gehalten, aber lauffähig bzw. direkt in ein Vite/CRA-Projekt übertragbar.
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).
Initial-Mount oder State-/Prop-Änderung
Funktionskörper wird ausgeführt (rein, ohne Seiteneffekte!), erzeugt neuen Element-/Fiber-Baum
React schreibt Änderungen ins DOM, setzt refs
läuft synchron, noch VOR dem Browser-Paint (blockierend)
läuft asynchron NACH dem Paint (nicht blockierend)
State-/Prop-Änderung → Cleanup-Funktion des vorherigen Effects (falls Dependencies sich geändert haben) → erneute Render-Phase → Commit-Phase → neuer Effect
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.
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. div → span) → 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.
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 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.
<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).
| Hook | Zweck | Typischer Einsatzfall |
|---|---|---|
useState | Lokaler, primitiver State innerhalb einer Komponente | Formularfeld, Zähler, Toggle-Zustand |
useEffect | Seiteneffekt nach dem Rendern/Commit ausführen (asynchron, nach Paint) | Datenabruf, Subscriptions, Timer, Logging |
useLayoutEffect | Seiteneffekt synchron vor dem Browser-Paint ausführen | DOM messen und Layout korrigieren, Tooltip-Positionierung, Scroll-Restauration |
useContext | Wert aus einem Context ohne Prop-Drilling lesen | Theme, angemeldeter User, Sprache/i18n |
useMemo | Ergebnis einer teuren Berechnung zwischenspeichern, bis sich Dependencies ändern | Große Listen filtern/sortieren, abgeleitete Werte |
useCallback | Funktionsreferenz stabil halten, bis sich Dependencies ändern | Callback an memoisierte Kindkomponente (React.memo) übergeben |
useRef | Veränderlicher Wert, der KEIN Re-Render auslöst; auch DOM-Knoten-Referenz | Fokus setzen, Timer-ID merken, vorherigen Wert speichern |
useReducer | Komplexere State-Übergänge über Actions/Reducer-Funktion steuern | Mehrstufige Formulare, State-Maschinen, viele zusammenhängende Felder |
useTransition | State-Update als "niedrige Priorität" markieren, UI bleibt währenddessen reaktionsfähig | Große Listen/Suchergebnisse filtern, ohne Tastatureingabe zu blockieren |
Custom Hook (useXyz) | Wiederverwendbare, zustandsbehaftete Logik aus mehreren Basis-Hooks kapseln | useDebounce, useFetch, useLocalStorage (siehe Abschnitt 13) |
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.
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).
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.
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).
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.
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.
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).
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.
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.
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.
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.
| Pattern | Idee | Wann sinnvoll |
|---|---|---|
| Children-Props | Beliebiger JSX-Inhalt wird über props.children "durchgereicht" | Layout-Wrapper, Card, Modal-Hülle |
| Compound Components | Mehrere 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 Props | Eine Prop ist eine Funktion, die JSX zurückgibt; die Komponente ruft sie mit internem State auf | Wiederverwendbare 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ückgibt | Cross-Cutting Concerns wie withAuth(Component), withLogging(Component) (heute meist durch Hooks ersetzt) |
Beispiel: Compound Components (Tabs)
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.
| Ebene | API | Lebensdauer / Sichtbarkeit | Typischer Einsatz |
|---|---|---|---|
| Lokaler State | useState / useReducer | Lebt nur innerhalb einer Komponenteninstanz, verschwindet beim Unmount | Formularfeld, Toggle, Zähler, UI-State |
| Context | createContext / useContext | Lebt so lange wie der Provider im Baum gemountet ist; sichtbar für alle Nachfahren des Providers | Theme, angemeldeter User, Sprache – Daten, die "quer" durch viele Komponenten gebraucht werden |
| Externer Store | Zustand, Redux Toolkit, Jotai u. Ä. | Lebt außerhalb des Komponentenbaums (Modul-Singleton); überlebt Mount/Unmount einzelner Komponenten, i. d. R. für die gesamte App-Session | Warenkorb, globaler App-State, komplexe/häufig aktualisierte Daten, die viele nicht verwandte Komponenten lesen |
Lokaler State
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
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
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.
| Ansatz | Vorteile | Nachteile | Wann 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) |
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.
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.
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>
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.
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.
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.
| Konstrukt | Beispiel | Bedeutung |
|---|---|---|
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 |
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>
</>
);
}
&&
{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.
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>
);
}
@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.
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).
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>
);
}
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>
);
}
| Kriterium | CSS-Klasse via ref (ohne State) | Bedingtes Rendern via State |
|---|---|---|
| Re-Render ausgelöst? | Nein – React-Baum bleibt unverändert | Ja – Komponente (und ggf. Kinder) rendert neu |
| DOM-Knoten / interner State des Kindbaums | Bleibt 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 Umschalten | Sehr günstig (reine CSS-Operation) | Etwas teurer (React-Reconciliation), in der Praxis meist vernachlässigbar |
| Zugänglichkeit von verstecktem Inhalt | Inhalt existiert weiterhin im DOM (ggf. aria-hidden nötig) | Inhalt existiert nicht im DOM, wenn ausgeblendet (sauberer für Screenreader) |
| Wann sinnvoll | Rein optische Zustände, hochfrequentes Umschalten, Animationen, wenn interner State erhalten bleiben soll | Wenn 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.
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.
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;
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.
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.
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.
// Button.module.css
// .primary { background: royalblue; color: white; }
import styles from "./Button.module.css";
function Button({ children }) {
return <button className={styles.primary}>{children}</button>;
}
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
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.
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.
{
"greeting": "Hallo, {{name}}!",
"nav": { "home": "Start", "contact": "Kontakt" }
}
{
"greeting": "Hello, {{name}}!",
"nav": { "home": "Home", "contact": "Contact" }
}
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;
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.
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", "");
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.
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 };
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
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-Array | Wann 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 Mount | Einmalige 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 haben | Datenabruf 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 Unmount | Timer 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.
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:
| Thema | Warum relevant |
|---|---|
| Server Components (RSC) & Frameworks wie Next.js | Verä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/useOptimistic | Neuer, 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.