Skip to content

React Spickzettel

JavaScript-Bibliothek zum Bauen von Benutzeroberflächen mit Komponenten.

01

JSX & Components

Funktions-Komponenten

React-Komponenten sind JavaScript-Funktionen, die JSX zurückgeben. Komponentennamen müssen mit einem Großbuchstaben beginnen (Kleinbuchstabe = HTML-Tags). Props werden als Attribute übergeben und im Parameter destrukturiert. JSX ist syntaktischer Zucker für React.createElement(). Gib immer ein einzelnes Root-Element zurück (oder verwende Fragments). Komponenten müssen rein sein — gleiche Props = gleiche Ausgabe.

react
// Basic function component
function Welcome({ name }) {
  return <h1>Hello, {name}!</h1>;
}

// Arrow function component
const Greeting = ({ name = 'Guest' }) => (
  <p>Welcome, {name}!</p>
);

// Composing components
function App() {
  return (
    <div>
      <Welcome name="Alice" />
      <Welcome name="Bob" />
      <Greeting />
    </div>
  );
}

JSX-Ausdrücke

JSX erlaubt das Einbetten von JavaScript-Ausdrücken in geschweiften Klammern {}. Du kannst Variablen, Funktionsaufrufe, ternäre Operatoren und jeden Ausdruck, der einen Wert zurückgibt, einsetzen. Statements (if, for, switch) sind nicht direkt erlaubt — verwende ternäre Operatoren oder IIFE. Boolean-Werte (true, false), null und undefined rendern als nichts. Zahlen und Strings rendern als Text. Objekte sind keine gültigen React-Children.

react
function Expression({ user, items }) {
  const fullName = user.first + ' ' + user.last;
  const itemCount = items.length;

  return (
    <div>
      {/* Expressions in curly braces */}
      <h1>{fullName}</h1>
      <p>{itemCount} items</p>

      {/* Conditional */}
      <p>{itemCount > 0 ? 'In stock' : 'Sold out'}</p>

      {/* Method calls */}
      <p>{fullName.toUpperCase()}</p>

      {/* Numbers and booleans render as nothing */}
      <p>{false}{null}{undefined}</p>
    </div>
  );
}

Fragments & Listen in JSX

Fragments (<>...</>) gruppieren mehrere Elemente, ohne zusätzliche DOM-Knoten hinzuzufügen — sauberer als das Einwickeln in ein <div>. Verwende <Fragment key={...}>, wenn du einen Key übergeben musst. Arrays von JSX-Elementen benötigen eindeutige key-Props. Während der Array-Index als Key für statische Listen funktioniert, verwende stabile IDs für dynamische Listen, um Rendering-Bugs zu verhindern. Fragments verbessern die Performance, indem sie unnötige Wrapper-Elemente reduzieren.

react
// Fragment: group without extra DOM node
function App() {
  return (
    <>
      <header>Header</header>
      <main>Content</main>
      <footer>Footer</footer>
    </>
  );
}

// Array of elements (needs keys)
function List() {
  const fruits = ['Apple', 'Banana', 'Cherry'];
  return (
    <ul>
      {fruits.map((fruit, i) => (
        <li key={i}>{fruit}</li>
      ))}
    </ul>
  );
}

Bedingtes Rendering

React bietet mehrere Muster für bedingtes Rendering. Early Returns für if/else-Logik. Ternärer Operator (cond ? A : B) für Entweder/Oder. Logical AND (cond && <Component/>) für Anzeigen/Verbergen. IIFE für komplexe Verzweigungen. Vermeide es, komplexe Logik in JSX einzubetten — extrahiere in Variablen oder Hilfsfunktionen. Für switch-Statements verwende ein Lookup-Objekt oder extrahiere in eine separate Funktion. Falsy-Werte (0, '') rendern, verwende also bei Zahlen den ternären Operator statt &&.

react
function Greeting({ isLoggedIn, user }) {
  // 1. If/else (use early return)
  if (!isLoggedIn) return <Login />;

  // 2. Ternary operator
  return (
    <div>
      {user ? <Dashboard user={user} /> : <Loading />}

      {/* 3. Logical AND (render if truthy) */}
      {user.isAdmin && <AdminPanel />}

      {/* 4. IIFE for complex logic */}
      {(() => {
        if (user.role === 'admin') return <Admin />;
        if (user.role === 'mod') return <Mod />;
        return <User />;
      })()}
    </div>
  );
}

Children & Render Props

Die children-Prop enthält Elemente zwischen öffnendem und schließendem Tag — essenziell für komponierbare Komponenten (Cards, Modals, Layouts). Render Props übergeben eine Funktion als Prop, die Daten empfängt und JSX zurückgibt — eine Alternative zu HOCs und Hooks zum Teilen von Logik. Während Render Props mit Hooks seltener sind, sind sie noch nützlich für Komponenten-Injektions-Muster. children ist eine spezielle Prop, die nicht explizit übergeben werden muss.

react
// children prop: content between tags
function Card({ title, children }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-body">{children}</div>
    </div>
  );
}

// Usage
<Card title="Profile">
  <p>Name: Alice</p>
  <p>Age: 30</p>
</Card>

// Render prop pattern
function DataProvider({ render }) {
  const data = fetchData();
  return <div>{render(data)}</div>;
}
02

Props

Props übergeben

Props sind read-only-Daten, die vom Eltern- zum Kind-Element übergeben werden. Sie können jeder JavaScript-Wert sein: Strings, Zahlen, Booleans, Arrays, Objekte oder Funktionen. String-Werte verwenden Anführungszeichen (name='Alice'), alle anderen Werte verwenden geschweifte Klammern (age={30}). Funktionen als Props ermöglichen Kind-zu-Eltern-Kommunikation (Callbacks). Props fließen abwärts — Kinder können Props nicht ändern. Für Two-Way-Data-Binding hebe den State zum gemeinsamen Eltern-Element.

react
// Parent passes props to child
function App() {
  return (
    <User
      name="Alice"
      age={30}
      isActive={true}
      tags={['admin', 'dev']}
      onClick={() => console.log('clicked')}
    />
  );
}

// Child receives props
function User({ name, age, isActive, tags, onClick }) {
  return (
    <div onClick={onClick}>
      <h1>{name}</h1>
      <p>Age: {age}</p>
      <p>Status: {isActive ? 'Active' : 'Inactive'}</p>
    </div>
  );
}

Default & optionale Props

Default-Prop-Werte werden via Destrukturierung gesetzt (param = defaultValue). Wenn eine Prop nicht übergeben wird, ist sie undefined. Verwende Short-Circuit (bio && <p>) oder den ternären Operator, um optionale Props bedingt zu rendern. PropTypes (Legacy) oder TypeScript-Interfaces können Prop-Typen zur Entwicklungszeit validieren. defaultProps (Klassen-Komponenten) ist für Funktions-Komponenten veraltet — verwende stattdessen Destrukturierungs-Defaults.

react
// Default values via destructuring
function Button({ color = 'blue', size = 'md', children }) {
  return (
    <button className={'btn btn-' + color + ' btn-' + size}>
      {children}
    </button>
  );
}

// Optional props (undefined if not passed)
function Profile({ name, bio }) {
  return (
    <div>
      <h1>{name}</h1>
      {bio && <p>{bio}</p>}
    </div>
  );
}

// Usage
<Button>Click</Button>           {/* color='blue', size='md' */}
<Profile name="Alice" />         {/* bio is undefined */}

Spread & Rest Props

Der Spread-Operator (...props) übergibt alle Props an ein Kind-Element — nützlich für Wrapper-Komponenten (HOCs, Styled Components). Der Rest-Operator sammelt verbleibende Props, nachdem spezifische destrukturiert wurden. Dieses Muster ist in Design-Systemen verbreitet, wo eine Wrapper-Komponente unbekannte Props an ein DOM-Element weitergibt. Vorsicht: Spreading kann explizite Attribute überschreiben — die Reihenfolge ist wichtig ({...props} className='x' vs. className='x' {...props}).

react
// Spread: pass all props to child
function Input(props) {
  return <input {...props} className="input" />;
}

// Usage
<Input type="text" placeholder="Name" value="Alice" />

// Rest: collect remaining props
function Button({ label, ...rest }) {
  return <button {...rest}>{label}</button>;
}

// Selective spreading
function Card({ title, children, ...divProps }) {
  return (
    <div {...divProps}>
      <h2>{title}</h2>
      {children}
    </div>
  );
}

Prop Types & TypeScript

TypeScript-Interfaces bieten Compile-Time-Typprüfung für Props — der empfohlene Ansatz für neue React-Projekte. Optionale Props verwenden ? (isActive?: boolean). PropTypes bieten Laufzeit-Validierung (nur in der Entwicklung) und sind nützlich für JavaScript-Projekte ohne TypeScript. isRequired stellt sicher, dass die Prop bereitgestellt wird. TypeScript fängt Typfehler vor der Laufzeit ab und ist daher für große Codebasen überlegen.

react
// TypeScript interface (recommended)
interface UserProps {
  name: string;
  age: number;
  isActive?: boolean;       // optional
  onClick: (id: number) => void;
}

function User({ name, age, isActive = true, onClick }: UserProps) {
  return <div onClick={() => onClick(1)}>{name}, {age}</div>;
}

// PropTypes (runtime checking, legacy)
import PropTypes from 'prop-types';
User.propTypes = {
  name: PropTypes.string.isRequired,
  age: PropTypes.number,
  isActive: PropTypes.bool,
};

Prop Drilling & Context

Prop Drilling tritt auf, wenn Props durch mehrere Komponenten-Schichten gereicht werden, die sie nicht verwenden. Für 2-3 Ebenen ist es akzeptabel. Für tiefere Bäume verwende Context API, State-Management-Bibliotheken (Redux, Zustand) oder Komponenten-Komposition. Komposition (Komponenten als Props oder Children übergeben) löst Drilling oft eleganter als Context. Frage: Braucht jede Zwischenkomponente diese Daten? Wenn nicht, überdenke deine Komponenten-Struktur.

react
// Prop drilling: passing through multiple levels
function App() {
  const [user, setUser] = useState(null);
  return <Layout user={user} />;
}

function Layout({ user }) {
  return <Sidebar user={user} />;
}

function Sidebar({ user }) {
  return <UserInfo user={user} />;
}

// Solution: Context API (see State Management section)
// Avoid drilling more than 2-3 levels
03

useState & State

Basis-useState

useState ist der fundamentale Hook, um Funktions-Komponenten State hinzuzufügen. Er gibt ein Array zurück: [aktuellerWert, setterFunktion]. Der Initialwert kann jeder Typ sein. Der Aufruf des Setters löst ein Re-Render mit dem neuen Wert aus. State-Updates sind asynchron — der Wert ändert sich nicht sofort nach dem Aufruf von setCount. Jede Komponenten-Instanz hat ihren eigenen unabhängigen State. Der Setter ist stabil (gleiche Referenz über Renders hinweg).

react
import { useState } from 'react';

function Counter() {
  // [currentValue, setterFunction] = useState(initialValue)
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount(count - 1)}>-1</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

Funktionale Updates

Wenn der neue State vom vorherigen State abhängt, verwende ein funktionales Update: setCount(prev => prev + 1). Dies garantiert, dass du mit dem neuesten State arbeitest, selbst wenn mehrere Updates gebatcht werden. Ohne funktionale Updates können schnelle aufeinanderfolgende Aufrufe veralteten State verwenden. React 18 batcht State-Updates automatisch (sogar in Promises und Timeouts), daher sind funktionale Updates essenziell für Korrektheit.

react
function Counter() {
  const [count, setCount] = useState(0);

  // BAD: may not work correctly with rapid updates
  const incrementBad = () => setCount(count + 1);

  // GOOD: functional update uses previous state
  const incrementGood = () => setCount(prev => prev + 1);

  // Batch updates
  const addThree = () => {
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
  };

  return <button onClick={addThree}>Count: {count}</button>;
}

State mit Objekten & Arrays

Mutiere niemals State direkt — erstelle immer ein neues Objekt/Array. Für Objekte verwende den Spread-Operator, um bestehende Eigenschaften zu kopieren: {...prev, [field]: value}. Für Arrays verwende Spread zum Hinzufügen ([...prev, newItem]), filter zum Entfernen und map zum Aktualisieren. React vergleicht Referenzen, um Änderungen zu erkennen — mutierte Objekte haben dieselbe Referenz, sodass React nicht re-rendert. Dies ist die Quelle #1 für React-Bugs bei Anfängern.

react
function Form() {
  const [form, setForm] = useState({ name: '', email: '', age: 0 });

  // BAD: mutates state directly
  // form.name = 'Alice'; setForm(form);

  // GOOD: spread to create new object
  const updateField = (field, value) => {
    setForm(prev => ({ ...prev, [field]: value }));
  };

  return (
    <input
      value={form.name}
      onChange={e => updateField('name', e.target.value)}
    />
  );
}

// Array state
function TodoList() {
  const [todos, setTodos] = useState([]);
  const addTodo = (text) => setTodos(prev => [...prev, { id: Date.now(), text }]);
  const removeTodo = (id) => setTodos(prev => prev.filter(t => t.id !== id));
}

Lazy Initial State

Wenn der initiale State eine teure Berechnung erfordert, übergebe eine Funktion an useState (Lazy Initialization). Die Funktion läuft nur beim ersten Render, nicht bei jedem Re-Render. Dies ist wichtig für das Parsen von localStorage, das Abrufen aus IndexedDB oder jedes CPU-intensive Setup. Die Funktionsform: useState(() => initialValue). Für einfache Werte (Zahlen, Strings) übergebe einfach den Wert direkt — Lazy Init ist unnötig.

react
import { useState } from 'react';

function ExpensiveInit() {
  // BAD: runs on every render (even though result is ignored)
  const [data, setData] = useState(computeExpensiveValue());

  // GOOD: lazy initialization — function runs only once
  const [data2, setData2] = useState(() => computeExpensiveValue());

  // Reading from localStorage
  const [user, setUser] = useState(() => {
    const saved = localStorage.getItem('user');
    return saved ? JSON.parse(saved) : null;
  });

  return <div>{data2}</div>;
}

Mehrere State-Variablen

Verwende mehrere useState-Aufrufe für unabhängige Werte statt eines großen Objekts. Dies macht Updates einfacher (kein Spread nötig) und verhindert unnötige Re-Renders. Gruppiere verwandte Werte in einem einzelnen State-Objekt (z. B. Formularfelder). Für komplexe State-Logik mit mehreren Unterwerten ziehe useReducer in Betracht. Faustregel: Wenn State-Updates unabhängig sind, verwende separate useState; wenn sie verwandt/abhängig sind, verwende useReducer oder ein einzelnes Objekt.

react
function LoginForm() {
  // Multiple independent state variables
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [rememberMe, setRememberMe] = useState(false);

  const handleSubmit = (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    // ... validation and submission
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={e => setEmail(e.target.value)} />
      <input type="password" value={password}
        onChange={e => setPassword(e.target.value)} />
      <button disabled={isSubmitting}>Submit</button>
    </form>
  );
}
04

useEffect & Side Effects

Basis-useEffect

useEffect führt Side Effects nach dem Render aus. Die Effect-Funktion läuft, nachdem die Komponente gemalt wurde. Die Cleanup-Funktion (zurückgegeben) läuft vor dem nächsten Effect und bei Unmount — essenziell für das Aufräumen von Timern, Subscriptions und Listenern. Das Dependency-Array steuert, wann der Effect erneut läuft: [] = einmal bei Mount, [dep] = wenn dep sich ändert, kein Array = jeder Render. Räume immer auf, um Memory Leaks zu verhindern.

react
import { useState, useEffect } from 'react';

function Timer() {
  const [seconds, setSeconds] = useState(0);

  // Runs after every render
  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);

    // Cleanup function runs before next effect or unmount
    return () => clearInterval(interval);
  }, []); // empty array = run once on mount

  return <p>Seconds: {seconds}</p>;
}

Dependency-Array

Das Dependency-Array ist kritisch für useEffect-Verhalten. Leeres Array [] = nur bei Mount (wie componentDidMount). Mit Dependencies [a, b] = läuft bei Mount und wenn a oder b sich ändert. Kein Array = jeder Render (selten gewollt). Fehlende Dependencies verursachen Stale Closures. Unnötige Dependencies verursachen übermäßige Re-Runs. Verwende die exhaustive-deps-ESLint-Regel, um Fehler zu finden. Jeder Wert aus dem Komponenten-Scope, der im Effect verwendet wird, sollte in den Deps sein.

react
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  // Runs once on mount (empty deps)
  useEffect(() => {
    console.log('Component mounted');
  }, []);

  // Runs when userId changes
  useEffect(() => {
    fetch('/api/users/' + userId)
      .then(r => r.json())
      .then(setUser);
  }, [userId]); // re-run when userId changes

  // Runs on every render (no deps) - rarely needed
  useEffect(() => {
    console.log('Every render');
  });

  return <div>{user?.name}</div>;
}

Cleanup & Subscriptions

Cleanup ist essenziell für Subscriptions, Event-Listener, Timer und WebSocket-Verbindungen. Ohne Cleanup erhältst du Memory Leaks und doppelte Handler. Die Cleanup-Funktion läuft: (1) vor dem nächsten Effect-Re-Run, (2) bei Komponenten-Unmount. Für WebSocket/Event-Listener entferne sie immer im Cleanup. Für State, der vom Effect abhängt, setze ihn im Cleanup zurück, um die Anzeige veralteter Daten aus einem vorherigen roomId zu vermeiden.

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

  useEffect(() => {
    const ws = new WebSocket('wss://chat.example.com/' + roomId);

    ws.onmessage = (event) => {
      setMessages(prev => [...prev, JSON.parse(event.data)]);
    };

    // Cleanup: close connection when roomId changes or unmount
    return () => {
      ws.close();
      setMessages([]); // reset for new room
    };
  }, [roomId]);

  // Window event listener
  useEffect(() => {
    const handleResize = () => console.log(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return <div>{messages.length} messages</div>;
}

Daten abrufen

Datenabruf in useEffect erfordert ein Cancellation-Flag, um das Setzen von State nach Unmount zu verhindern (verursacht React-Warnungen). Das 'cancelled'-Flag stellt sicher, dass setUsers/setError/setLoading nur laufen, wenn die Komponente noch gemountet ist. Für Produktions-Apps ziehe eine Data-Fetching-Bibliothek (React Query, SWR) in Betracht, die Caching, Deduplikation und Race Conditions automatisch behandelt. Das leere Dependency-Array [] stellt sicher, dass das Fetchen einmal bei Mount passiert.

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

  useEffect(() => {
    let cancelled = false;

    const fetchData = async () => {
      try {
        setLoading(true);
        const res = await fetch('/api/users');
        const data = await res.json();
        if (!cancelled) setUsers(data);
      } catch (err) {
        if (!cancelled) setError(err.message);
      } finally {
        if (!cancelled) setLoading(false);
      }
    };

    fetchData();
    return () => { cancelled = true; };
  }, []);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

useLayoutEffect vs. useEffect

useEffect läuft asynchron, nachdem der Browser malt — Nutzer können ein kurzes Flackern sehen, wenn du DOM-Elemente misst. useLayoutEffect läuft synchron nach DOM-Mutationen, aber vor dem Paint — verhindert visuelles Flackern. Verwende useLayoutEffect für DOM-Messungen (getBoundingClientRect, Scroll-Position), die das Layout beeinflussen. Verwende useEffect für alles andere (es blockiert nicht das Malen). Auf dem Server warnt useLayoutEffect — verwende das useIsomorphicLayoutEffect-Muster.

react
import { useState, useEffect, useLayoutEffect } from 'react';

function Tooltip({ text }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  // useEffect: runs AFTER paint (user may see flash)
  useEffect(() => {
    const rect = document.getElementById('tip').getBoundingClientRect();
    setPosition({ x: rect.x, y: rect.y });
  }, [text]);

  // useLayoutEffect: runs BEFORE paint (no flash)
  useLayoutEffect(() => {
    const rect = document.getElementById('tip').getBoundingClientRect();
    setPosition({ x: rect.x, y: rect.y });
  }, [text]);

  return <div id="tip" style={{ left: position.x, top: position.y }}>{text}</div>;
}
05

useRef, useMemo & useCallback

useRef-Grundlagen

useRef gibt ein veränderbares Objekt { current: value } zurück, das über Renders hinweg persists. Im Gegensatz zu State löst das Ändern von ref.current KEIN Re-Render aus. Häufige Verwendungen: (1) Zugriff auf DOM-Elemente (via ref-Attribut), (2) Speichern veränderbarer Werte, die das Rendering nicht beeinflussen (Timer, vorherige Werte), (3) Speichern des neuesten Werts zur Verwendung in Callbacks. Das ref-Objekt hat über Renders hinweg dieselbe Identität. Der Initialwert wird an useRef(initialValue) übergeben.

react
import { useRef } from 'react';

function FocusInput() {
  // ref to access DOM element
  const inputRef = useRef(null);

  const focus = () => inputRef.current.focus();
  const clear = () => {
    inputRef.current.value = '';
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focus}>Focus</button>
      <button onClick={clear}>Clear</button>
    </div>
  );
}

useRef für veränderbare Werte

useRef speichert veränderbare Werte, die über Renders hinweg persists, ohne Re-Renders auszulösen. Dies ist perfekt für: Timer-IDs, WebSocket-Referenzen, das Verfolgen vorheriger State-Werte und das Zählen von Renders. Da das Ändern von ref.current kein Re-Render auslöst, aktualisiert sich die UI nicht, wenn du es änderst — verwende State für Werte, die die UI beeinflussen sollen. Das Render-Count-Muster (ref.current++) ist nützlich zum Debuggen, sollte aber nicht in Produktionslogik verwendet werden.

react
function Stopwatch() {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef(null);
  const renderCount = useRef(0);

  // Track render count (doesn't trigger re-render)
  renderCount.current++;

  const start = () => {
    if (intervalRef.current) return;
    intervalRef.current = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);
  };

  const stop = () => {
    clearInterval(intervalRef.current);
    intervalRef.current = null;
  };

  return (
    <div>
      <p>{seconds}s (render #{renderCount.current})</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  );
}

useMemo

useMemo memoisiert (cacht) einen berechneten Wert und berechnet nur neu, wenn sich Dependencies ändern. Verwende es für teure Berechnungen (Filtern, Sortieren, komplexe Mathematik), um ein Neu-Ausführen bei jedem Render zu vermeiden. Das Dependency-Array funktioniert wie bei useEffect. Überbeanspruchung kann die Performance beeinträchtigen (Memoisierung hat eigenen Overhead) — memoisiere nur genuinely teure Operationen. useMemo ist auch nützlich, um Objekt-Referenzen zu erhalten und Kind-Re-Renders zu verhindern.

react
import { useState, useMemo } from 'react';

function ProductList({ products, filter }) {
  const [search, setSearch] = useState('');

  // Memoize expensive computation
  const filtered = useMemo(() => {
    console.log('Filtering...');
    return products
      .filter(p => p.category === filter)
      .filter(p => p.name.includes(search));
  }, [products, filter, search]); // recompute only when these change

  // Memoize a value
  const totalPrice = useMemo(() =>
    filtered.reduce((sum, p) => sum + p.price, 0),
  [filtered]);

  return (
    <div>
      <input value={search} onChange={e => setSearch(e.target.value)} />
      <p>Total: ${totalPrice}</p>
      {filtered.map(p => <div key={p.id}>{p.name}</div>)}
    </div>
  );
}

useCallback

useCallback memoisiert eine Funktion und gibt dieselbe Referenz über Renders hinweg zurück, außer wenn sich Dependencies ändern. Dies verhindert unnötige Re-Renders memoisierter Kind-Komponenten (eingewickelt in memo()). Ohne useCallback erzeugt jeder Eltern-Render eine neue Funktionsreferenz, was dazu führt, dass memo()-Kinder re-rendern. Verwende useCallback, wenn du Callbacks an optimierte Kind-Komponenten übergibst. Wie useMemo nicht überbeanspruchen — nur für Funktionen, die als Props an memoisierte Kinder übergeben werden.

react
import { useState, useCallback, memo } from 'react';

// Memoized child component
const Button = memo(function Button({ onClick, label }) {
  console.log('Button rendered');
  return <button onClick={onClick}>{label}</button>;
});

function App() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // Without useCallback: new function every render
  // const handleClick = () => setCount(c => c + 1);

  // With useCallback: stable function reference
  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []); // empty deps = stable forever

  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} />
      <Button onClick={handleClick} label="Click" />
      <p>Count: {count}</p>
    </div>
  );
}

Refs weiterleiten

forwardRef erlaubt Eltern-Komponenten, eine ref an das DOM-Element einer Kind-Komponente zu übergeben. useImperativeHandle passt an, was die ref freilegt — statt des DOM-Knotens kannst du spezifische Methoden freilegen (focus, clear, getValue). Dies ist nützlich, um wiederverwendbare Input-Komponenten mit imperativen APIs zu erstellen. React 19 vereinfachte refs (ref ist jetzt eine reguläre Prop), aber forwardRef wird noch für Bibliotheken benötigt. Vermeide übermäßige imperative Handles — bevorzuge deklarative Props.

react
import { useRef, forwardRef, useImperativeHandle } from 'react';

// forwardRef: pass ref to child component
const FancyInput = forwardRef(function FancyInput(props, ref) {
  return <input ref={ref} className="fancy" {...props} />;
});

// useImperativeHandle: expose specific methods
const CustomInput = forwardRef(function CustomInput(props, ref) {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    clear: () => { inputRef.current.value = ''; },
    getValue: () => inputRef.current.value,
  }));

  return <input ref={inputRef} />;
});

// Usage
function App() {
  const ref = useRef(null);
  return (
    <>
      <CustomInput ref={ref} />
      <button onClick={() => ref.current.focus()}>Focus</button>
      <button onClick={() => ref.current.clear()}>Clear</button>
    </>
  );
}
06

useReducer & Context

useReducer-Grundlagen

useReducer ist eine Alternative zu useState für komplexe State-Logik. Ein Reducer ist eine reine Funktion: (state, action) => newState. Actions beschreiben, was passiert ist; der Reducer entscheidet, wie der State aktualisiert wird. Dieses Muster macht State-Übergänge vorhersagbar und testbar. Dispatch ist stabil (gleiche Referenz). Gib immer ein neues State-Objekt zurück (niemals mutieren). Der Default-Case sollte einen Fehler für unbekannte Actions werfen. Verwende useReducer, wenn State mehrere Unterwerte hat oder der nächste State von komplexer Logik abhängt.

react
import { useReducer } from 'react';

// Reducer function: (state, action) => newState
function counterReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    case 'set':
      return { count: action.payload };
    default:
      throw new Error('Unknown action: ' + action.type);
  }
}

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
      <button onClick={() => dispatch({ type: 'set', payload: 10 })}>Set 10</button>
    </div>
  );
}

Komplexer Reducer

Komplexe Reducer verwalten mehrere verwandte State-Stücke. Jeder Action-Typ behandelt einen spezifischen State-Übergang. Spreade immer den vorherigen State ({...state}), um unverwandte Felder zu erhalten. Für verschachtelte Updates (wie das Umschalten eines Todos) verwende map, um ein neues Array mit dem aktualisierten Item zu erstellen. Reducer müssen rein sein — keine Side Effects, keine API-Aufrufe. Extrahiere den Reducer in eine separate Datei für Testbarkeit. Ziehe Bibliotheken wie Redux Toolkit für sehr komplexen State in Betracht.

react
const initialState = {
  todos: [],
  filter: 'all',
  loading: false,
};

function todoReducer(state, action) {
  switch (action.type) {
    case 'add':
      return { ...state, todos: [...state.todos, action.todo] };
    case 'toggle':
      return {
        ...state,
        todos: state.todos.map(t =>
          t.id === action.id ? { ...t, done: !t.done } : t
        ),
      };
    case 'delete':
      return { ...state, todos: state.todos.filter(t => t.id !== action.id) };
    case 'set_filter':
      return { ...state, filter: action.filter };
    case 'set_loading':
      return { ...state, loading: action.loading };
    default:
      return state;
  }
}

function TodoApp() {
  const [state, dispatch] = useReducer(todoReducer, initialState);
  // dispatch({ type: 'add', todo: { id: 1, text: 'Learn React', done: false } })
}

Context API

Context API teilt State über den Komponenten-Baum, ohne Prop Drilling. Erstelle mit createContext(defaultValue). Wickele Consumer in Provider mit einer value-Prop ein. Konsumiere mit useContext(Context). Context-Wert-Änderungen lösen Re-Renders aller Consumer aus. Für Performance teile Contexts (ThemeContext, UserContext), sodass Komponenten nur re-rendern, wenn ihr spezifischer Context sich ändert. Der Default-Wert wird verwendet, wenn kein Provider den Consumer umschließt.

react
import { createContext, useContext, useState } from 'react';

// 1. Create context with default value
const ThemeContext = createContext('light');
const UserContext = createContext(null);

// 2. Provider component
function App() {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState(null);

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <UserContext.Provider value={{ user, setUser }}>
        <Page />
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

// 3. Consume context
function Page() {
  const { theme } = useContext(ThemeContext);
  const { user } = useContext(UserContext);
  return <div className={'page theme-' + theme}>Hello {user?.name}</div>;
}

Context mit Reducer

Die Kombination von Context mit useReducer erzeugt ein leichtgewichtiges globales State-Management-System (Mini-Redux). Der Provider legt sowohl State als auch dispatch offen. Ein Custom Hook (useStore) bietet Fehlerbehandlung, wenn außerhalb des Providers verwendet. Dieses Muster ist großartig für mittelgroße Apps. Für sehr große Apps mit häufigen Updates ziehe das Aufteilen von Contexts oder die Verwendung von Redux/Zustand in Betracht, um zu vermeiden, dass alle Consumer bei jeder State-Änderung re-rendern.

react
import { createContext, useContext, useReducer } from 'react';

// Combine Context + useReducer for global state
const StoreContext = createContext(null);

function StoreProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <StoreContext.Provider value={{ state, dispatch }}>
      {children}
    </StoreContext.Provider>
  );
}

// Custom hook for easy consumption
function useStore() {
  const context = useContext(StoreContext);
  if (!context) throw new Error('useStore must be used within StoreProvider');
  return context;
}

// Usage
function Component() {
  const { state, dispatch } = useStore();
  return <button onClick={() => dispatch({ type: 'action' })}>Click</button>;
}

useContext-Performance

Wenn sich der Context-Wert ändert, re-rendern ALLE Consumer — selbst wenn sie nur einen kleinen Teil des Werts verwenden. Zur Optimierung: (1) Teile Contexts, sodass Komponenten nur abonnieren, was sie benötigen. (2) Memoisiere den Context-Wert mit useMemo, um Re-Renders zu verhindern, wenn sich der Wert nicht tatsächlich geändert hat. (3) Verwende Selektoren (use-context-selector-Bibliothek) für feingranulare Subscriptions. Für hochfrequente Updates (wie Mausposition) kann Context Performance-Probleme verursachen — ziehe refs oder externe Stores in Betracht.

react
// SPLIT contexts for performance
const ThemeContext = createContext();
const UserContext = createContext();
const CartContext = createContext();

// Each provider manages its own state
function App() {
  return (
    <ThemeProvider>
      <UserProvider>
        <CartProvider>
          <App />
        </CartProvider>
      </UserProvider>
    </ThemeProvider>
  );
}

// Component only re-renders when its context changes
function ThemedButton() {
  const { theme } = useContext(ThemeContext);
  // Won't re-render when user or cart changes
  return <button className={theme}>Button</button>;
}

// Memoize context value to prevent unnecessary re-renders
function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  const value = useMemo(() => ({ user, setUser }), [user]);
  return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}
07

Events & Forms

Event-Handling

React-Events verwenden camelCase (onClick, nicht onclick). Das Event-Objekt ist ein SyntheticEvent (Wrapper um das native Event). e.preventDefault() stoppt Standardverhalten (Form-Submit, Link-Navigation). e.stopPropagation() verhindert Event-Bubbling. Um Parametern an Handler zu übergeben, verwende Arrow Functions: onClick={() => handleDelete(id)}. Vermeide es, komplexe Handler inline zu definieren — extrahiere sie für Lesbarkeit. React-Events sind gepoolt (pre-17), rufe also e.persist() auf, wenn du asynchronen Zugriff benötigst.

react
function App() {
  // Click event
  const handleClick = (e) => {
    e.preventDefault();
    console.log('Button clicked', e.target);
  };

  // With parameters (use arrow function)
  const handleDelete = (id) => {
    console.log('Delete item', id);
  };

  return (
    <div>
      <button onClick={handleClick}>Click</button>
      <button onClick={() => handleDelete(42)}>Delete</button>
      <div onMouseEnter={() => console.log('hover')}
           onMouseLeave={() => console.log('leave')}>
        Hover me
      </div>
    </div>
  );
}

Controlled Inputs

Controlled Inputs haben ihren Wert durch React-State kontrolliert. Die value-Prop setzt den Input-Wert, und onChange aktualisiert den State. Dies macht React zur 'Single Source of Truth' für Formulardaten. Jeder Tastenanschlag löst ein State-Update und Re-Render aus. Für komplexe Formulare kann dies verbose sein — ziehe Bibliotheken wie React Hook Form oder Formik in Betracht. Controlled Inputs ermöglichen Echtzeit-Validierung und dynamisches Verhalten. Verwende immer onChange mit value (oder readOnly), um React-Warnungen zu vermeiden.

react
function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Email:', email, 'Password:', password);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={e => setEmail(e.target.value)}
        placeholder="Email"
      />
      <input
        type="password"
        value={password}
        onChange={e => setPassword(e.target.value)}
        placeholder="Password"
      />
      <button type="submit">Login</button>
    </form>
  );
}

Formular mit mehreren Feldern

Für Formulare mit vielen Feldern verwende ein einzelnes State-Objekt und eine generische handleChange-Funktion. Das name-Attribut auf jedem Input entspricht dem State-Key. Der Handler verwendet berechnete Eigenschaftsnamen ([name]: value), um das korrekte Feld zu aktualisieren. Für Checkboxen verwende checked statt value. Dieses Muster reduziert Boilerplate erheblich. Für File-Inputs verwende unkontrollierte Inputs (sie können nicht vollständig kontrolliert werden). Ziehe React Hook Form für komplexe Formulare mit Validierung in Betracht.

react
function RegistrationForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
    country: 'us',
    agree: false,
  });

  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: type === 'checkbox' ? checked : value,
    }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="username" value={formData.username} onChange={handleChange} />
      <input name="email" type="email" value={formData.email} onChange={handleChange} />
      <select name="country" value={formData.country} onChange={handleChange}>
        <option value="us">USA</option>
        <option value="uk">UK</option>
      </select>
      <label>
        <input type="checkbox" name="agree" checked={formData.agree} onChange={handleChange} />
        Agree to terms
      </label>
      <button type="submit">Register</button>
    </form>
  );
}

Uncontrolled Inputs

Unkontrollierte Inputs verwenden refs, um direkt auf den DOM-Wert zuzugreifen, ohne React-State. Die defaultValue-Prop setzt den Initialwert (nicht value). Dies ist einfacher für Formulare, die keine Echtzeit-Validierung oder dynamisches Verhalten benötigen. File-Inputs müssen unkontrolliert sein (ihr Wert ist aus Sicherheitsgründen read-only). Unkontrollierte Inputs sind auch nützlich zur Integration mit nicht-React-Code. Der Trade-off: Du kannst Input nicht einfach in Echtzeit validieren oder transformieren. Bevorzuge für die meisten Fälle kontrollierte Inputs.

react
import { useRef } from 'react';

function UncontrolledForm() {
  const emailRef = useRef(null);
  const passwordRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Email:', emailRef.current.value);
    console.log('Password:', passwordRef.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={emailRef} type="email" defaultValue="" />
      <input ref={passwordRef} type="password" defaultValue="" />
      <button type="submit">Submit</button>
    </form>
  );
}

// File input (must be uncontrolled)
function FileUpload() {
  const fileRef = useRef(null);
  return <input ref={fileRef} type="file" />;
}

Validierung & Fehlerbehandlung

Formular-Validierung kann beim Submit oder bei jeder Änderung erfolgen. Die validate-Funktion gibt ein errors-Objekt zurück — leer bedeutet gültig. Zeige Fehler bedingt neben jedem Feld an. Für bessere UX validiere bei blur (nachdem der Nutzer das Feld verlässt) statt bei jedem Tastenanschlag. Bibliotheken wie React Hook Form + Zod oder Formik + Yup bieten robuste Validierungs-Schemata, Fehler-Management und Touch/Blur-Tracking. Validiere immer auch auf dem Server — Client-Validierung ist für UX, nicht Sicherheit.

react
function ValidatedForm() {
  const [values, setValues] = useState({ email: '', password: '' });
  const [errors, setErrors] = useState({});

  const validate = () => {
    const errs = {};
    if (!values.email) errs.email = 'Email is required';
    else if (!/\S+@\S+\.\S+/.test(values.email)) errs.email = 'Invalid email';
    if (!values.password) errs.password = 'Password is required';
    else if (values.password.length < 8) errs.password = 'Min 8 characters';
    return errs;
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    const errs = validate();
    setErrors(errs);
    if (Object.keys(errs).length === 0) {
      console.log('Form valid', values);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={values.email}
        onChange={e => setValues(v => ({ ...v, email: e.target.value }))} />
      {errors.email && <span className="error">{errors.email}</span>}
      <input type="password" value={values.password}
        onChange={e => setValues(v => ({ ...v, password: e.target.value }))} />
      {errors.password && <span className="error">{errors.password}</span>}
      <button type="submit">Submit</button>
    </form>
  );
}
08

Listen & bedingtes Rendering

Listen rendern

Verwende .map(), um Arrays in JSX-Elemente zu transformieren. Jedes Element benötigt eine eindeutige key-Prop — verwende stabile IDs (todo.id), nicht Array-Indizes. Keys helfen React zu identifizieren, welche Items sich ändern (hinzugefügt, entfernt, umgeordnet) für effiziente DOM-Updates. Die Verwendung des Index als Key verursacht Bugs, wenn Listenelemente umgeordnet oder am Anfang eingefügt werden. Für leere Listen rendere eine Fallback-Nachricht. Ziehe useMemo für gefilterte/sortierte Listen in Betracht, um Neu-Berechnung bei jedem Render zu vermeiden.

react
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <span>{todo.text}</span>
          <button onClick={() => toggle(todo.id)}>
            {todo.done ? 'Undo' : 'Done'}
          </button>
        </li>
      ))}
    </ul>
  );
}

// Filtering and sorting
function FilteredList({ items, filter }) {
  const visible = items
    .filter(item => item.category === filter)
    .sort((a, b) => a.name.localeCompare(b.name));

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

Keys erklärt

Keys müssen unter Geschwistern (demselben Eltern-Element) eindeutig sein, können aber über verschiedene Listen hinweg wiederholt werden. Keys helfen Reacts Reconciliation-Algorithmus: Wenn sich ein Key ändert, zerstört und erneuert React die Komponente (verliert State). Mit Index-Keys verschiebt das Einfügen eines Items am Anfang alle Indizes, was dazu führt, dass React alles neu rendert. Mit stabilen ID-Keys rendert React nur das neue Item. Keys müssen nicht global eindeutig sein — nur innerhalb der Liste. Verwende keine zufälligen Keys (Math.random()) — sie ändern sich bei jedem Render.

react
// GOOD: stable, unique keys
{todos.map(todo => (
  <TodoItem key={todo.id} todo={todo} />
))}

// BAD: index as key (causes bugs with reordering)
{todos.map((todo, index) => (
  <TodoItem key={index} todo={todo} />
))}

// When index keys are OK:
// - Static list (never reordered/filtered)
// - List items have no state
// - List is never prepended to

// Key must be unique among siblings
function List() {
  return (
    <div>
      {users.map(u => <User key={u.id} user={u} />)}
      {posts.map(p => <Post key={p.id} post={p} />)}
      {/* IDs can repeat across different lists */}
    </div>
  );
}

Muster für bedingtes Rendering

Es gibt mehrere Muster für bedingtes Rendering. Early Returns sind am saubersten für Guard Clauses (Loading, Error, Auth). Element-Variablen funktionieren für if/else in der Mitte der Komponente. Ternärer Operator (cond ? A : B) für Entweder/Oder in JSX. Logical AND (cond && <X/>) für Anzeigen/Verbergen. Objekt-Lookup für switch-ähnliches Verhalten. Vermeide verschachtelte ternäre Operatoren — extrahiere in Variablen oder Komponenten. Für Zahlen verwende den ternären Operator statt && (0 && <X/> rendert 0).

react
function UserDashboard({ user, loading, error }) {
  // 1. Early returns for loading/error states
  if (loading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;
  if (!user) return <Login />;

  // 2. Element variables
  let greeting;
  if (user.isAdmin) {
    greeting = <h1>Welcome Admin {user.name}</h1>;
  } else {
    greeting = <h1>Welcome {user.name}</h1>;
  }

  return (
    <div>
      {greeting}

      {/* 3. Ternary for either/or */}
      {user.hasNotifications ? <NotificationBadge /> : null}

      {/* 4. && for show/hide */}
      {user.isAdmin && <AdminPanel />}

      {/* 5. Switch via object lookup */}
      {{ free: <FreePlan />, pro: <ProPlan />, enterprise: <EnterprisePlan /> }
        [user.plan]}
    </div>
  );
}

Listen filtern & suchen

Durchsuchbare/filterbare Listen kombinieren useState für Filter mit useMemo für Performance. Die Filter-Funktion prüft sowohl Query als auch Kategorie. Behandle immer den leeren State (keine Ergebnisse). Für große Listen (1000+ Items) ziehe Virtualisierung (react-window, react-virtualized) in Betracht, um nur sichtbare Items zu rendern. Debounce Search-Input für API-Aufrufe. Case-insensitive Suche verwendet toLowerCase(). Für komplexes Filtern extrahiere in eine separate Funktion oder einen Custom Hook.

react
function SearchableList({ items }) {
  const [query, setQuery] = useState('');
  const [category, setCategory] = useState('all');

  // Memoize filtered results
  const filtered = useMemo(() => {
    return items.filter(item => {
      const matchesQuery = item.name.toLowerCase().includes(query.toLowerCase());
      const matchesCategory = category === 'all' || item.category === category;
      return matchesQuery && matchesCategory;
    });
  }, [items, query, category]);

  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <select value={category} onChange={e => setCategory(e.target.value)}>
        <option value="all">All</option>
        <option value="food">Food</option>
        <option value="tech">Tech</option>
      </select>

      {filtered.length === 0 ? (
        <p>No results found</p>
      ) : (
        <ul>
          {filtered.map(item => <li key={item.id}>{item.name}</li>)}
        </ul>
      )}
    </div>
  );
}

Dynamische Komponenten

Dynamisches Komponenten-Rendering verwendet ein Lookup-Objekt, um Typen Komponenten zuzuordnen. Dies ist häufig in CMS-gesteuertem Content, Form-Buildern und Page-Builderm. Die Komponenten-Map vermeidet lange switch/if-Ketten. Behandle unbekannte Typen immer mit einer Fallback-Komponente. Spread Props ({...block.props}), um alle Eigenschaften an die dynamische Komponente zu übergeben. Dieses Muster ist flexibel und erweiterbar — einen neuen Block-Typ hinzuzufügen erfordert nur das Hinzufügen zur Map. Schreibe die Variable groß (Component), damit JSX sie als Komponente behandelt.

react
// Render different components based on type
const componentMap = {
  text: TextBlock,
  image: ImageBlock,
  video: VideoBlock,
  quote: QuoteBlock,
};

function ContentRenderer({ blocks }) {
  return (
    <div>
      {blocks.map(block => {
        const Component = componentMap[block.type];
        if (!Component) return <UnknownBlock key={block.id} type={block.type} />;

        return <Component key={block.id} {...block.props} />;
      })}
    </div>
  );
}

// Usage
const blocks = [
  { id: 1, type: 'text', props: { content: 'Hello' } },
  { id: 2, type: 'image', props: { src: 'pic.jpg', alt: 'Picture' } },
];
09

Performance Optimization

React.memo

React.memo wickelt eine Komponente ein, um Re-Renders zu verhindern, wenn sich Props nicht geändert haben (flacher Vergleich). Verwende es für Komponenten, die oft mit denselben Props rendern. Das zweite Argument ist eine benutzerdefinierte Vergleichsfunktion: return true, um Re-Render zu überspringen, false, um zu re-rendern. memo hilft nur, wenn die Komponente teuer zu rendern ist oder ein Kind eines häufig rendernden Eltern-Elements ist. Wickele nicht jede Komponente ein — Memoisierung hat Overhead. Kombiniere mit useCallback/useMemo für maximalen Effekt.

react
import { memo } from 'react';

// Memoized component: only re-renders if props change
const ExpensiveCard = memo(function ExpensiveCard({ title, content }) {
  console.log('Card rendered');
  return (
    <div className="card">
      <h2>{title}</h2>
      <p>{content}</p>
    </div>
  );
});

// Custom comparison function
const MyComponent = memo(function MyComponent(props) {
  return <div>{props.value}</div>;
}, (prevProps, nextProps) => {
  // Return true if props are equal (skip re-render)
  return prevProps.value === nextProps.value;
});

Code Splitting

Code Splitting reduziert die initiale Bundle-Größe, indem Komponenten on-demand geladen werden. React.lazy + Suspense ermöglicht dynamische Imports. Die fallback-Prop wird angezeigt, während die Komponente lädt. Route-basiertes Splitting (Lazy-Loading von Seiten-Komponenten) ist am wirkungsvollsten. Komponenten-basiertes Splitting ist nützlich für schwere Komponenten (Charts, Editoren), die nicht sofort benötigt werden. Jeder Lazy-Import erzeugt einen separaten Chunk. Verwende React.lazy für Default-Exports; für Named-Exports wickele in ein Modul ein.

react
import { lazy, Suspense } from 'react';

// Lazy load component (code-split)
const AdminPanel = lazy(() => import('./AdminPanel'));
const Dashboard = lazy(() => import('./Dashboard'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Router>
        <Route path="/admin" element={<AdminPanel />} />
        <Route path="/dashboard" element={<Dashboard />} />
      </Router>
    </Suspense>
  );
}

// Route-based splitting (most common)
// Component-based splitting
const HeavyChart = lazy(() => import('./HeavyChart'));

function Page({ showChart }) {
  return (
    <div>
      {showChart && (
        <Suspense fallback={<Spinner />}>
          <HeavyChart />
        </Suspense>
      )}
    </div>
  );
}

Virtualisierung für lange Listen

Virtualisierung rendert nur die sichtbaren Items in einer langen Liste und verbessert die Performance drastisch. react-window und react-virtualized sind beliebte Bibliotheken. Statt 10.000 DOM-Knoten zu rendern, werden nur ~12 (die sichtbaren) gerendert, mit einem scrollbaren Container. Dies reduziert DOM-Größe und Render-Zeit. Verwende Virtualisierung für Listen mit 100+ Items. Der Trade-off: komplexere Implementierung, potenzielle Probleme mit Search/Find (Items nicht im DOM). Listen variabler Höhe benötigen VariableSizeList.

react
import { FixedSizeList } from 'react-window';

// Virtualized list: only renders visible items
function BigList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      {items[index].name}
    </div>
  );

  return (
    <FixedSizeList
      height={600}
      width="100%"
      itemCount={items.length}
      itemSize={50}
    >
      {Row}
    </FixedSizeList>
  );
}

// Without virtualization: rendering 10,000 items is slow
// With virtualization: only ~12 visible items are rendered

Debouncing & Throttling

Debouncing verzögert die Ausführung bis zu einer Pause in der Aktivität (z. B. Nutzer hört auf zu tippen). Throttling begrenzt die Ausführung auf einmal pro Intervall. Beide verhindern übermäßige API-Aufrufe oder Berechnungen. Der useDebounce-Hook aktualisiert den debounceten Wert erst, nachdem der Nutzer für die angegebene Verzögerung aufgehört hat zu tippen. Dies ist essenziell für Search-Inputs, Resize-Handler und Scroll-Events. Für Throttling verwende eine Bibliothek wie lodash.throttle oder implementiere mit Timestamps. Räume Timer immer in useEffect auf.

react
import { useState, useEffect } from 'react';

// Debounce hook: delays execution until user stops typing
function useDebounce(value, delay) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debounced;
}

function SearchInput() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 300);

  // API call only fires when user stops typing for 300ms
  useEffect(() => {
    if (debouncedQuery) {
      fetch('/api/search?q=' + debouncedQuery);
    }
  }, [debouncedQuery]);

  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

Profiling & Optimierung

Die Profiler-Komponente misst Render-Zeiten. phase ist 'mount' oder 'update'. actualDuration ist die Render-Zeit in Millisekunden. Verwende React DevTools Profiler für visuelle Flame-Charts. Vor der Optimierung profile, um tatsächliche Bottlenecks zu finden — rate nicht. Häufige Performance-Probleme: (1) unnötige Re-Renders (behebe mit memo/useMemo/useCallback), (2) teure Berechnungen (behebe mit useMemo), (3) große Listen (behebe mit Virtualisierung), (4) große Bundles (behebe mit Code Splitting). Premature Optimization verschwendet Zeit — messe zuerst.

react
import { Profiler } from 'react';

function App() {
  const onRender = (id, phase, actualDuration) => {
    console.log(id + ' ' + phase + ': ' + actualDuration + 'ms');
  };

  return (
    <Profiler id="App" onRender={onRender}>
      <ExpensiveComponent />
    </Profiler>
  );
}

/* Optimization checklist:
1. React.memo for expensive components
2. useMemo for expensive calculations
3. useCallback for props passed to memoized children
4. Code splitting (lazy) for routes
5. Virtualization for long lists
6. Debounce rapid events (search, resize)
7. Avoid inline objects/functions as props
8. Use keys correctly in lists
9. Profile with React DevTools Profiler
10. Check unnecessary re-renders with why-did-you-render
*/
10

Patterns & Error Boundaries

Custom Hooks

Custom Hooks extrahieren wiederverwendbare zustandsbehaftete Logik in eine Funktion mit dem Präfix 'use'. Sie können andere Hooks aufrufen. Custom Hooks sind die primäre Methode, um Logik zwischen Komponenten zu teilen (ersetzen HOCs und Render Props). Gib ein Objekt für mehrere Werte zurück oder einen Wert/Array für einzelne Werte. Behandle immer Loading- und Error-States. Das Cancellation-Muster (cancelled-Flag) verhindert State-Updates nach Unmount. Benenne Hooks mit dem 'use'-Präfix, damit ESLint-Regeln funktionieren.

react
import { useState, useEffect } from 'react';

// Reusable data fetching hook
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    fetch(url)
      .then(res => {
        if (!res.ok) throw new Error('HTTP ' + res.status);
        return res.json();
      })
      .then(data => { if (!cancelled) { setData(data); setError(null); }})
      .catch(err => { if (!cancelled) setError(err.message); })
      .finally(() => { if (!cancelled) setLoading(false); });
    return () => { cancelled = true; };
  }, [url]);

  return { data, loading, error };
}

// Usage
function UserProfile({ id }) {
  const { data: user, loading, error } = useFetch('/api/users/' + id);
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  return <h1>{user.name}</h1>;
}

useLocalStorage-Hook

useLocalStorage persistiert State in localStorage. Der Lazy-Initializer liest beim Mount aus localStorage. Der useEffect schreibt in localStorage, wann immer sich der Wert ändert. Das try/catch behandelt Quota-Exceeded-Fehler und JSON-Parse-Fehler (korrupte Daten). Dieses Muster funktioniert für jeden persistenten State: Themes, Nutzer-Präferenzen, Entwurf-Inhalte. Für SSR-Kompatibilität prüfe typeof window !== 'undefined'. Für Cross-Tab-Sync lausche auf das 'storage'-Event. Ähnliche Hooks: useSessionStorage, useCookie.

react
import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const saved = localStorage.getItem(key);
      return saved ? JSON.parse(saved) : initialValue;
    } catch {
      return initialValue;
    }
  });

  useEffect(() => {
    try {
      localStorage.setItem(key, JSON.stringify(value));
    } catch (e) {
      console.error('LocalStorage error:', e);
    }
  }, [key, value]);

  return [value, setValue];
}

// Usage
function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  return (
    <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
      Current: {theme}
    </button>
  );
}

Error Boundaries

Error Boundaries fangen Fehler in Render/Lifecycle-Methoden von Kind-Komponenten ab und verhindern, dass die gesamte App abstürzt. Sie müssen Klassen-Komponenten sein (noch kein Hook-Äquivalent). getDerivedStateFromError aktualisiert State, um Fallback-UI anzuzeigen. componentDidCatch protokolliert Fehler (sende an Sentry, LogRocket usw.). Error Boundaries fangen NICHT ab: Event-Handler, asynchronen Code, setTimeout, Fehler in der Boundary selbst. Wickele spezifische Bereiche ein, um Ausfälle zu isolieren. Der 'Try again'-Button setzt den Error-State zurück.

react
import { Component } from 'react';

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

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

  componentDidCatch(error, errorInfo) {
    console.error('Caught error:', error, errorInfo);
    // Send to error reporting service
    // logErrorToService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        this.props.fallback || (
          <div>
            <h1>Something went wrong.</h1>
            <p>{this.state.error?.message}</p>
            <button onClick={() => this.setState({ hasError: false })}>
              Try again
            </button>
          </div>
        )
      );
    }
    return this.props.children;
  }
}

// Usage: wrap components
<ErrorBoundary fallback={<ErrorPage />}>
  <App />
</ErrorBoundary>

Higher-Order Components (HOC)

Higher-Order Components (HOCs) sind Funktionen, die eine Komponente nehmen und eine erweiterte zurückgeben. Sie waren das primäre Muster zum Teilen von Logik vor Hooks. Häufige Verwendungen: Authentifizierung, Loading-States, Theming. HOCs können 'Wrapper Hell' (tief verschachtelte Komponenten) und Prop-Kollisionen verursachen. Für neuen Code bevorzuge Custom Hooks — sie sind einfacher, komponierbarer und fügen nichts zum Komponenten-Baum hinzu. HOCs sind noch nützlich für Klassen-Komponenten oder bei der Integration mit Bibliotheken, die sie erfordern.

react
// HOC: function that takes a component and returns a new one
function withLoading(Component) {
  return function WithLoading({ isLoading, ...props }) {
    if (isLoading) return <div>Loading...</div>;
    return <Component {...props} />;
  };
}

// HOC for authentication
function withAuth(Component) {
  return function WithAuth(props) {
    const { user } = useContext(AuthContext);
    if (!user) return <Redirect to="/login" />;
    return <Component {...props} user={user} />;
  };
}

// Usage
const UserList = withLoading(withAuth(BaseUserList));

// Note: Prefer hooks over HOCs for new code
// HOCs are mainly for class components or library compatibility

Compound Components

Compound Components erlauben Nutzern, eine komplexe Komponente aus einfachen Teilen zu komponieren. Das Eltern-Element (Select) stellt Context bereit, und Kind-Komponenten (Trigger, Options, Option) konsumieren ihn. Dieses Muster wird von Bibliotheken wie Radix UI, Headless UI und React Aria verwendet. Vorteile: flexible API (Nutzer können Teile umordnen/auslassen), implizites State-Sharing via Context, sauberes JSX. Die Komponenten werden als statische Eigenschaften angehängt (Select.Trigger). Dies ist ein fortgeschrittenes Muster — verwende es für wiederverwendbare UI-Bibliotheken, nicht für einmalige Komponenten.

react
// Compound components: components that work together
function Select({ children, value, onChange }) {
  const [isOpen, setIsOpen] = useState(false);
  const context = { value, onChange, isOpen, setIsOpen };

  return (
    <SelectContext.Provider value={context}>
      <div className="select">{children}</div>
    </SelectContext.Provider>
  );
}

Select.Trigger = function Trigger({ children }) {
  const { isOpen, setIsOpen } = useContext(SelectContext);
  return <button onClick={() => setIsOpen(!isOpen)}>{children}</button>;
};

Select.Options = function Options({ children }) {
  const { isOpen } = useContext(SelectContext);
  return isOpen ? <div className="options">{children}</div> : null;
};

Select.Option = function Option({ value, children }) {
  const { onChange, setIsOpen } = useContext(SelectContext);
  return (
    <div onClick={() => { onChange(value); setIsOpen(false); }}>
      {children}
    </div>
  );
}

// Usage: clean, declarative API
<Select value={val} onChange={setVal}>
  <Select.Trigger>Choose...</Select.Trigger>
  <Select.Options>
    <Select.Option value="a">Option A</Select.Option>
    <Select.Option value="b">Option B</Select.Option>
  </Select.Options>
</Select>
11

Custom Hooks

useFetch-Hook

Custom Hooks extrahieren wiederverwendbare zustandsbehaftete Logik in eine Funktion mit dem Präfix 'use'. useFetch kapselt Daten-Abruf mit Loading/Error-States. Der AbortController bricht In-Flight-Requests ab, wenn die Komponente unmountet oder sich die URL ändert (verhindert Race Conditions und Memory Leaks). Schließe immer Cleanup in useEffect für asynchrone Operationen ein. Custom Hooks können andere Hooks aufrufen (useState, useEffect, useContext). Sie sind die primäre Methode, um Logik zwischen Komponenten zu teilen, ohne Render Props oder HOCs. Benenne sie mit dem 'use'-Präfix, damit Reacts Rules-of-Hooks-Linter funktioniert.

react
function useFetch(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let abortController = new AbortController();
    setLoading(true);
    fetch(url, { ...options, signal: abortController.signal })
      .then((res) => {
        if (!res.ok) throw new Error(res.statusText);
        return res.json();
      })
      .then((data) => { setData(data); setError(null); })
      .catch((err) => {
        if (err.name !== "AbortError") setError(err.message);
      })
      .finally(() => setLoading(false));
    return () => abortController.abort();
  }, [url]);

  return { data, loading, error };
}

// Usage
function Profile({ userId }) {
  const { data, loading, error } = useFetch(`/api/users/${userId}`);
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  return <div>{data.name}</div>;
}

useLocalStorage-Hook

useLocalStorage synchronisiert React-State mit localStorage. Der Lazy-Initializer liest nur beim ersten Render aus localStorage. Der useEffect schreibt in localStorage, wann immer sich der Wert ändert. Das try/catch behandelt Fälle, in denen localStorage voll oder deaktiviert ist (Private Browsing). Dieser Hook macht persistenten State so einfach wie useState. Für Cross-Tab-Synchronisation füge einen storage-Event-Listener hinzu. Für SSR-Sicherheit schütze den window-Zugriff. Dieses Muster funktioniert auch für sessionStorage — tausche einfach die API.

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

  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (e) {
      console.error("LocalStorage write failed:", e);
    }
  }, [key, value]);

  return [value, setValue];
}

// Usage: persists state across page reloads
function Settings() {
  const [theme, setTheme] = useLocalStorage("theme", "light");
  const [fontSize, setFontSize] = useLocalStorage("fontSize", 14);
  return (
    <div>
      <button onClick={() => setTheme("dark")}>Dark</button>
      <button onClick={() => setTheme("light")}>Light</button>
    </div>
  );
}

useDebounce-Hook

useDebounce verzögert das Aktualisieren eines Werts, bis der Nutzer für die angegebene Verzögerung aufgehört hat zu tippen. Dies ist essenziell für Search-Inputs, Autosave und API-Aufrufe, die durch Nutzereingaben ausgelöst werden — es verhindert übermäßige Aufrufe bei jedem Tastenanschlag. Die Cleanup-Funktion löscht den Timeout, wenn sich der Wert erneut ändert, bevor die Verzögerung abläuft. Der debouncete Wert aktualisiert sich nur nach der Pause und löst Downstream-Effects (wie API-Aufrufe) seltener aus. Für sofortige Ausführung mit einem Trailing-Call verwende stattdessen useThrottle. Kombiniere mit useFetch für effizientes Search-as-you-type.

react
function useDebounce(value, delay = 500) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);
  return debounced;
}

// Usage: debounce search input
function Search() {
  const [query, setQuery] = useState("");
  const debouncedQuery = useDebounce(query, 300);

  useEffect(() => {
    if (debouncedQuery) {
      fetch(`/api/search?q=${debouncedQuery}`)
        .then((res) => res.json())
        .then(setResults);
    }
  }, [debouncedQuery]);

  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

usePrevious-Hook

usePrevious nutzt die Tatsache, dass useEffect nach dem Render läuft — ref.current hält während des Renders noch den alten Wert und aktualisiert sich dann auf den neuen Wert. Dies ist ein häufiges Muster zum Vergleichen aktuellen und vorherigen States. useWindowSize verfolgt Viewport-Dimensionen mit einem Resize-Listener. Räume Event-Listener immer im useEffect-Return auf, um Memory Leaks zu verhindern. Diese Utility-Hooks demonstrieren, wie Custom Hooks DOM-bezogene Logik kapseln, Komponenten sauberer machen und die Logik wiederverwendbar und testbar machen.

react
function usePrevious(value) {
  const ref = useRef(null);
  useEffect(() => {
    ref.current = value; // update AFTER render
  }, [value]);
  return ref.current; // returns previous value during render
}

// Usage: compare current vs previous
function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);

  return (
    <div>
      <p>Now: {count}, before: {prevCount}</p>
      {count > prevCount && <p>Increased!</p>}
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}

// useWindowSize hook
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });
  useEffect(() => {
    const handler = () =>
      setSize({ width: innerWidth, height: innerHeight });
    window.addEventListener("resize", handler);
    return () => removeEventListener("resize", handler);
  }, []);
  return size;
}

useToggle & useClipboard

useToggle vereinfacht Boolean-State mit einer Toggle-Funktion, eingewickelt in useCallback für stabile Identität. useClipboard wickelt die Clipboard-API mit einem 'copied'-Feedback-State, der sich nach einem Timeout automatisch zurücksetzt. Diese kleinen Utility-Hooks reduzieren Boilerplate und standardisieren häufige Muster über deine App hinweg. Das useCallback in beiden Hooks verhindert unnötige Re-Renders memoisierter Kinder. Eine Bibliothek kleiner, fokussierter Hooks (useToggle, useClipboard, useMediaQuery, useOnClickOutside) zu bauen, beschleunigt die Entwicklung und stellt konsistentes Verhalten sicher.

react
function useToggle(initial = false) {
  const [on, setOn] = useState(initial);
  const toggle = useCallback(() => setOn((p) => !p), []);
  return [on, toggle, setOn];
}

function useClipboard(timeout = 2000) {
  const [copied, setCopied] = useState(false);
  const copy = useCallback((text) => {
    navigator.clipboard.writeText(text).then(() => {
      setCopied(true);
      setTimeout(() => setCopied(false), timeout);
    });
  }, [timeout]);
  return [copied, copy];
}

// Usage
function CopyButton({ text }) {
  const [copied, copy] = useClipboard();
  return (
    <button onClick={() => copy(text)}>
      {copied ? "Copied!" : "Copy"}
    </button>
  );
}

function Modal({ children }) {
  const [isOpen, toggle] = useToggle(false);
  return (
    <>
      <button onClick={toggle}>Open</button>
      {isOpen && <div className="modal">{children}</div>}
    </>
  );
}
12

Portals

Ein Portal erstellen

createPortal rendert Children in einen DOM-Knoten außerhalb der Hierarchie der aktuellen Komponente (typischerweise document.body). Dies ist essenziell für Modals, Tooltips und Dropdowns, die Eltern-CSS-Einschränkungen entkommen müssen (overflow: hidden, z-index-Stacking-Contexts, transform, das neue Contexts erzeugt). Trotz des Renderns an anderer Stelle im DOM funktioniert das React-Event-Bubbling des Portals so, als wäre es im originalen Baum — onClick-Handler auf Vorfahren feuern noch. Dies gibt dir das Beste aus beiden Welten: visuelles Entkommen aus Eltern-Einschränkungen, aber logischer Event-Flow erhalten.

react
import { createPortal } from "react-dom";

function Modal({ children, onClose }) {
  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        <button onClick={onClose}>✕</button>
        {children}
      </div>
    </div>,
    document.body  // render target outside the DOM hierarchy
  );
}

// Usage: the modal renders at body level,
// escaping any overflow:hidden or z-index stacking contexts
function App() {
  const [show, setShow] = useState(false);
  return (
    <div style={{ overflow: "hidden", position: "relative" }}>
      <button onClick={() => setShow(true)}>Open Modal</button>
      {show && <Modal onClose={() => setShow(false)}>Hello!</Modal>}
    </div>
  );
}

Modal mit Portal & Focus Trap

Ein Produktions-Modal benötigt mehr als nur ein Portal: Focus-Management (Focus im Modal einfangen, bei Close wiederherstellen), Escape-Key-Handling, Body-Scroll-Locking und Click-Outside-to-Close. Diese Implementierung speichert das zuvor fokussierte Element, fokussiert das Modal beim Öffnen und stellt den Focus bei Close wieder her — essenziell für Screen-Reader-Nutzer. Body overflow hidden verhindert Hintergrund-Scrolling. Die Cleanup-Funktion stellt alles wieder her. Für vollständiges Focus-Trapping (Tab-Cycling innerhalb des Modals) verwende eine Bibliothek wie focus-trap-react. Gib immer null zurück, wenn geschlossen, um aus dem DOM zu entfernen.

react
function Modal({ isOpen, onClose, children }) {
  const modalRef = useRef(null);

  useEffect(() => {
    if (!isOpen) return;
    const modal = modalRef.current;
    const previouslyFocused = document.activeElement;
    modal.focus();

    const handleKey = (e) => {
      if (e.key === "Escape") onClose();
    };
    document.addEventListener("keydown", handleKey);

    // Prevent body scroll
    document.body.style.overflow = "hidden";

    return () => {
      document.removeEventListener("keydown", handleKey);
      document.body.style.overflow = "";
      previouslyFocused.focus(); // restore focus
    };
  }, [isOpen, onClose]);

  if (!isOpen) return null;

  return createPortal(
    <div className="overlay" onClick={onClose}>
      <div ref={modalRef} tabIndex={-1} className="modal">
        {children}
      </div>
    </div>,
    document.body
  );
}

Tooltips mit Portals

Tooltips profitieren von Portals, weil sie über Eltern-Container überlaufen und Clipping vermeiden müssen. Die Tooltip-Position wird aus getBoundingClientRect() des Triggers berechnet und mit position: fixed auf Body-Ebene gerendert. Dies vermeidet z-index- und overflow-Probleme. Für dynamische Positionierung (Flip bei Bildschirmrand-Nähe) verwende eine Bibliothek wie Floating UI (früher Popper.js). Das Portal stellt sicher, dass der Tooltip nie durch overflow: hidden-Vorfahren abgeschnitten wird. Fixed-Positionierungs-Koordinaten sind relativ zum Viewport, was die Berechnung unkompliziert macht.

react
function Tooltip({ children, text }) {
  const [visible, setVisible] = useState(false);
  const [coords, setCoords] = useState({ x: 0, y: 0 });
  const targetRef = useRef(null);

  const show = () => {
    const rect = targetRef.current.getBoundingClientRect();
    setCoords({ x: rect.left, y: rect.top - 40 });
    setVisible(true);
  };

  return (
    <>
      <span
        ref={targetRef}
        onMouseEnter={show}
        onMouseLeave={() => setVisible(false)}
      >
        {children}
      </span>
      {visible && createPortal(
        <div style={{ position: "fixed", left: coords.x, top: coords.y }}
             className="tooltip">
          {text}
        </div>,
        document.body
      )}
    </>
  );
}

// Usage
<Tooltip text="Click to save">💾</Tooltip>

Dropdown-Menüs mit Portals

Dropdown-Menüs stehen vor denselben overflow/z-index-Problemen wie Tooltips. Portals lösen das visuelle Problem. Der Click-Outside-Handler prüft, ob das Click-Ziel außerhalb der Trigger-ref liegt. Der Capture-Phase-Scroll-Listener (true als drittes Argument) schließt das Menü bei jedem Scroll, um zu verhindern, dass sich das Menü vom Trigger löst. Für Produktion verwende Floating UI, das Edge-Erkennung, Flipping, Shifting und automatische Positionierungs-Updates bei Scroll/Resize behandelt. Portals + richtige Positionierungs-Logik = robuste Dropdowns, die in jedem Layout-Kontext funktionieren.

react
function Dropdown({ trigger, children }) {
  const [open, setOpen] = useState(false);
  const [pos, setPos] = useState({ top: 0, left: 0 });
  const ref = useRef(null);

  const handleOpen = () => {
    const rect = ref.current.getBoundingClientRect();
    setPos({ top: rect.bottom + 4, left: rect.left });
    setOpen(true);
  };

  useEffect(() => {
    if (!open) return;
    const handleClick = (e) => {
      if (!ref.current?.contains(e.target)) setOpen(false);
    };
    const handleScroll = () => setOpen(false); // close on scroll
    document.addEventListener("mousedown", handleClick);
    window.addEventListener("scroll", handleScroll, true);
    return () => {
      document.removeEventListener("mousedown", handleClick);
      window.removeEventListener("scroll", handleScroll, true);
    };
  }, [open]);

  return (
    <>
      <div ref={ref} onClick={handleOpen}>{trigger}</div>
      {open && createPortal(
        <div style={{ position: "fixed", ...pos }} className="dropdown">
          {children}
        </div>,
        document.body
      )}
    </>
  );
}

Portal-Event-Bubbling

Ein Schlüssel-Feature von React Portals: Event-Bubbling folgt dem React-Komponenten-Baum, nicht dem DOM-Baum. onClick auf einer Eltern-Komponente feuert sogar dann, wenn das Kind zu document.body portalt wurde. Das bedeutet, dass Context, State und Event-Delegation alle natürlich funktionieren. CSS-Vererbung kreuzt jedoch NICHT die Portal-Grenze — Stile auf dem Eltern-Element kaskadieren nicht zu portalem Content, da sie in verschiedenen DOM-Teilbäumen sind. Du musst CSS explizit anwenden (via Klassen oder CSS-Variablen auf :root), um Portal-Content zu stylen. Diese Trennung ist für Modals/Tooltips meist wünschenswert.

react
function PortalExample() {
  // Despite rendering in document.body, events bubble
  // through the React tree, not the DOM tree
  return (
    <div onClick={() => console.log("Parent clicked!")}>
      <p>Click the button — parent handler fires!</p>
      {createPortal(
        <button onClick={() => console.log("Button clicked!")}>
          I'm in a portal
        </button>,
        document.body
      )}
    </div>
  );
}
// Clicking logs: "Button clicked!" then "Parent clicked!"

// This means context, state, and event delegation
// all work as if the portal were inline

// But CSS inheritance does NOT cross the portal boundary:
// document.body styles won't inherit into the portal content
// unless you explicitly apply them
13

Suspense & Lazy Loading

React.lazy & Suspense

React.lazy importiert eine Komponente dynamisch und erzeugt ein separates Bundle, das on-demand geladen wird (Code Splitting). Wickele Lazy-Komponenten in <Suspense> mit einem fallback (Loading-State) ein, das gezeigt wird, während der Chunk heruntergeladen wird. Dies reduziert die initiale Bundle-Größe — Nutzer laden nur Code für Seiten, die sie besuchen. Jeder lazy()-Aufruf erzeugt einen separaten Chunk. Für route-basiertes Splitting lazy-loade jede Seiten-Komponente. Das fallback kann jeder React-Node sein (Spinner, Skeleton, Text). Suspense kann mehrere Lazy-Komponenten umwickeln — das fallback zeigt, bis alle bereit sind.

react
import { lazy, Suspense } from "react";

// Lazy-load component (code-split)
const Dashboard = lazy(() => import("./Dashboard"));
const Settings = lazy(() => import("./Settings"));

function App() {
  return (
    <Suspense fallback={<div>Loading page...</div>}>
      <nav>
        <button onClick={() => setPage("dash")}>Dashboard</button>
        <button onClick={() => setPage("settings")}>Settings</button>
      </nav>
      {page === "dash" && <Dashboard />}
      {page === "settings" && <Settings />}
    </Suspense>
  );
}

Verschachtelte Suspense

Verschachtelte Suspense-Grenzen erzeugen einen 'Schäl'-Effekt, bei dem Content progressiv freigelegt wird, während jeder Chunk lädt. Äußere Suspense zeigt zuerst ihr fallback; während innere Komponenten laden, werden sie unabhängig freigegeben. Dies verhindert, dass eine einzelne langsame Komponente die gesamte Seite blockiert. Platziere Suspense-Grenzen strategisch: um Route-Level-Seiten (grob), um Hauptbereiche (mittel) und um unabhängige Widgets (fein). Zu viele Grenzen erzeugen ruckeliges Laden; zu wenige erzeugen lange Wartezeiten. Der Schlüssel ist, Grenzen an nutzerwahrnehmbare Content-Einheiten anzupassen.

react
<Suspense fallback={<PageSkeleton />}>
  <Header />
  <Suspense fallback={<MainSkeleton />}>
    <MainContent />  {/* loads first */}
    <Suspense fallback={<CommentsSkeleton />}>
      <Comments />  {/* loads independently, doesn't block MainContent */}
    </Suspense>
  </Suspense>
  <Sidebar />
</Suspense>

// Suspense "peeling" effect:
// 1. PageSkeleton shows
// 2. Header + Sidebar load → PageSkeleton peels away
// 3. MainSkeleton shows until MainContent loads
// 4. MainContent shows, CommentsSkeleton shows
// 5. Comments load → everything visible

Lazy mit Error Boundaries

Lazy Loading kann fehlschlagen (Netzwerkprobleme, Deployments, die Chunk-URLs invalidieren). Error Boundaries fangen diese Fehler ab und zeigen Fallback-UI. Wickele Suspense + lazy immer in eine ErrorBoundary ein. Der componentDidCatch protokolliert Fehler für Monitoring. Für Retry-Logik kannst du den State der ErrorBoundary zurücksetzen oder die Seite neu laden. Ein häufiges Muster ist ein Retry-Button, der den Chunk neu importiert. Ohne Error Boundaries lässt ein fehlgeschlagener Chunk-Lade die gesamte App abstürzen. Dies ist kritisch für Produktion — Netzwerk-Zuverlässigkeit ist nie 100%.

react
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  componentDidCatch(error, info) {
    console.error("Chunk load failed:", error, info);
  }
  render() {
    if (this.state.hasError) {
      return (
        <div>
          <p>Failed to load. {this.state.error.message}</p>
          <button onClick={() => window.location.reload()}>
            Retry
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}

// Wrap lazy components: network failures need handling
<ErrorBoundary>
  <Suspense fallback={<Loader />}>
    <LazyComponent />
  </Suspense>
</ErrorBoundary>

Daten-Abruf mit Suspense

React 19s use()-Hook ermöglicht Suspense für Daten-Abruf. Im Gegensatz zu useEffect suspendiert use() die Komponente, bis das Promise resolved — die nächstgelegene Suspense-Grenze zeigt ihr fallback. Mehrere use()-Aufrufe in derselben Komponente werden gleichzeitig aufgelöst (paralleles Fetchen). Dies eliminiert manuelles Loading-State-Management. Das Promise kann außerhalb von React gecacht werden, um ein Neu-Fetchen bei Re-Render zu verhindern. Hinweis: use() kann nur in Render oder innerhalb von Hooks aufgerufen werden. Für React 18 verwende Bibliotheken wie React Query oder SWR, die sich in Suspense integrieren.

react
// React 18+ Suspense for data fetching (experimental)
import { use } from "react"; // React 19+

// Wrap a promise with use()
function UserProfile({ userId }) {
  // 'use' suspends until the promise resolves
  const user = use(fetchUser(userId));
  return <div>{user.name}</div>;
}

function fetchUser(id) {
  return fetch(`/api/users/${id}`).then((r) => r.json());
}

// Parent provides Suspense boundary
function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <UserProfile userId={1} />
    </Suspense>
  );
}

// Concurrent: multiple suspends resolve together
function Dashboard() {
  const user = use(fetchUser(1));
  const posts = use(fetchPosts(user.id));
  // Both fetch in parallel, Suspense shows until all resolve
  return <div>{user.name}: {posts.length} posts</div>;
}

Suspense List (Orchestrierung)

SuspenseList orchestriert die Freigabe-Reihenfolge mehrerer Suspense-Grenzen. revealOrder='forwards' zeigt Items in Reihenfolge (Item 2 wird nicht freigegeben, bis Item 1 bereit ist, selbst wenn 2 zuerst lädt) — verhindert Content-Springen. 'together' wartet auf alle, bevor freigegeben wird. 'backwards' gibt von unten nach oben frei. tail='collapsed' verbirgt Loading-States für noch nicht gestartete Items; 'hidden' verbirgt alle Fallbacks. Dies ist nützlich für Feeds und Listen, bei denen die Reihenfolge wichtig ist. Hinweis: SuspenseList war experimentell und seine API kann sich ändern — prüfe aktuelle React-Docs auf Verfügbarkeit.

react
// SuspenseList controls reveal order of multiple Suspense
import { SuspenseList, Suspense } from "react";

function Article({ id }) {
  const data = use(fetchArticle(id));
  return <article>{data.title}</article>;
}

function Feed() {
  return (
    <SuspenseList revealOrder="forwards" tail="collapsed">
      {/* "forwards": reveals top-to-bottom as they load */}
      {/* "together": waits for all, reveals together */}
      {/* "backwards": reveals bottom-to-top */}

      <Suspense fallback={<Skeleton />}>
        <Article id={1} />
      </Suspense>
      <Suspense fallback={<Skeleton />}>
        <Article id={2} />
      </Suspense>
      <Suspense fallback={<Skeleton />}>
        <Article id={3} />
      </Suspense>
    </SuspenseList>
  );
}
14

React Router

Basis-Routing

React Router v6 verwendet <BrowserRouter> als Root, <Routes> zur Definition des Route-Matchings und <Route> mit der element-Prop (nicht component). <Link> erstellt Navigations-Links, die die History-API verwenden (kein Page-Reload). Dynamische Segmente (:id) werden via useParams() abgerufen. path='*' ist ein Catch-All für 404s. Routes werden nach Best Match gematcht, nicht nach Reihenfolge. Für URL-Search-Params (?q=search) verwende useSearchParams(). BrowserRouter erfordert Server-Konfiguration, um index.html für alle Routes zu serven (SPA-Fallback).

react
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
        <Link to="/users/123">User 123</Link>
      </nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/users/:id" element={<UserPage />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}

function UserPage() {
  const { id } = useParams();
  return <h1>User ID: {id}</h1>;
}

Verschachtelte Routes & Outlet

Verschachtelte Routes erstellen Layout-Hierarchien. Das element der Eltern-Route muss <Outlet /> enthalten, wo Kind-Routes rendern. Die Index-Route rendert am Pfad des Eltern-Elements. Tief verschachtelte Routes (users/:id) erstellen verschachtelte Layouts — Users-Layout umschließt UserDetail. Dies ist mächtig für Dashboards mit persistenten Sidebars/Headern. useOutlet() gibt Zugriff auf das Kind-Element. Die URL /users/123 rendert Layout → Users → UserDetail, wobei jedes sein Layout beiträgt. Dies ersetzt manuelles bedingtes Rendering von Layouts.

react
function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="about" element={<About />} />
          <Route path="users" element={<Users />}>
            <Route path=":id" element={<UserDetail />} />
          </Route>
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

function Layout() {
  return (
    <div>
      <nav>Navigation here</nav>
      <Outlet />  {/* Child routes render here */}
    </div>
  );
}

function Users() {
  return (
    <div>
      <h2>Users</h2>
      <Outlet />  {/* Nested :id route renders here */}
    </div>
  );
}

Navigation & Redirects

useNavigate gibt eine Funktion für programmatische Navigation zurück. navigate('/path', { replace: true }) ersetzt die History (kein Back-Button). Übergib state, um Daten zur nächsten Route zu tragen (z. B. wohin nach Login zurückzukehren). <Navigate> ist die deklarative Redirect-Komponente — verwende sie in Render für Auth-Guards. NavLink bietet isActive zum Stylen aktiver Links. useLocation gibt die aktuelle URL, pathname, search, hash und state zurück. Für Redirects nach Aktionen (Form-Submit) verwende navigate. Für bedingte Redirects in Render verwende <Navigate>.

react
import { useNavigate, Navigate, NavLink, useLocation } from "react-router-dom";

function Login() {
  const navigate = useNavigate();
  const location = useLocation();

  const handleLogin = async () => {
    await auth.login();
    // Redirect to intended page or home
    const from = location.state?.from || "/";
    navigate(from, { replace: true });
  };
  return <button onClick={handleLogin}>Login</button>;
}

// Declarative redirect
function ProtectedRoute({ user, children }) {
  if (!user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  return children;
}

// NavLink: active styling
<NavLink to="/about" className={({ isActive }) =>
  isActive ? "nav-active" : "nav"
}>
  About
</NavLink>

Loader & Daten-Laden

React Router v6.4+ (Data Router) fügt Loader (laufen vor Route-Render) und Actions (behandeln Form-Submissions) hinzu. useLoaderData() greift auf Loader-Daten zu — kein useEffect mehr für Route-Daten-Abruf. Loader laufen parallel für verschachtelte Routes. errorElement fängt Fehler aus Loadern/Actions ab. Actions verarbeiten Form-Submissions via <Form method='post'> — useActionData() gibt das Ergebnis zurück. Dieses Muster (inspiriert von Remix) co-lokiert Daten-Logik mit Routes. Die Data-APIs erfordern createBrowserRouter/createHashRouter, nicht <BrowserRouter>.

react
import { createBrowserRouter, RouterProvider } from "react-router-dom";

const router = createBrowserRouter([
  {
    path: "/users/:id",
    element: <UserPage />,
    loader: async ({ params }) => {
      const res = await fetch(`/api/users/${params.id}`);
      if (!res.ok) throw new Response("Not found", { status: 404 });
      return res.json();
    },
    errorElement: <ErrorPage />,
  },
]);

function UserPage() {
  const user = useLoaderData(); // data from loader
  return <h1>{user.name}</h1>;
}

// Action for form submissions
{
  path: "/users/new",
  element: <NewUser />,
  action: async ({ request }) => {
    const formData = await request.formData();
    const res = await fetch("/api/users", {
      method: "POST",
      body: formData,
    });
    return redirect(`/users/${res.id}`);
  },
}

function App() {
  return <RouterProvider router={router} />;
}

Route Guards & geschützte Routes

Route Guards schützen Routes basierend auf Auth-State oder Rollen. RequireAuth leitet unauthentifizierte Nutzer zu Login um und bewahrt das Ziel in location.state für Post-Login-Redirect. RequireRole fügt rollenbasierte Zugriffskontrolle hinzu. Komponiere Guards, indem du sie verschachtelst (RequireAuth > RequireRole > component). Für Layout-Routes kannst du auch element={<RequireAuth><Outlet/></RequireAuth>} verwenden, um alle Kind-Routes auf einmal zu schützen. Prüfe Auth immer auch auf dem Server — Client-seitige Guards sind für UX, nicht Sicherheit. Das Muster skaliert auf jede Bedingung: Subscription, Feature Flags usw.

react
function RequireAuth({ children }) {
  const { user } = useAuth();
  const location = useLocation();

  if (!user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  return children;
}

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/login" element={<Login />} />
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="dashboard" element={
            <RequireAuth><Dashboard /></RequireAuth>
          } />
          <Route path="admin" element={
            <RequireAuth><RequireRole role="admin"><Admin /></RequireRole></RequireAuth>
          } />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

function RequireRole({ role, children }) {
  const { user } = useAuth();
  if (user?.role !== role) return <Navigate to="/forbidden" />;
  return children;
}
15

State Management (Context & Redux)

Context-API-Muster

Context bietet globalen State ohne Prop Drilling. Erstelle einen Context, wickele Consumer in einen Provider ein und greife via useContext zu. Der Custom useAuth-Hook fügt Fehlerprüfung hinzu und ist die empfohlene API-Oberfläche. Context ist ideal für niederfrequente Updates (Auth, Theme, Locale). Für hochfrequente State-Änderungen verursacht Context, dass alle Consumer bei jeder Änderung re-rendern — verwende useReducer für komplexen State oder teile Contexts. Co-lokiere den Provider immer mit dem State, den er verwaltet. Context-Wert sollte mit useMemo/useCallback memoisiert werden, wenn er Funktionen enthält.

react
const AuthContext = createContext(null);

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const login = async (credentials) => {
    const user = await api.login(credentials);
    setUser(user);
  };
  const logout = () => setUser(null);
  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error("useAuth must be inside AuthProvider");
  return ctx;
}

// Usage
function Navbar() {
  const { user, logout } = useAuth();
  return user
    ? <button onClick={logout}>Logout {user.name}</button>
    : <Link to="/login">Login</Link>;
}

// Wrap app: <AuthProvider><App /></AuthProvider>

useReducer + Context

useReducer + Context ist das empfohlene Muster für komplexen globalen State ohne externe Bibliotheken. Der Reducer zentralisiert State-Logik (vorhersagbare, testbare Übergänge). Der Provider memoisiert den Wert, um unnötige Re-Renders zu verhindern. Abgeleitete Werte (total) werden in useMemo berechnet. Dieses Muster behandelt Cart, Form-State, Multi-Step-Wizards usw. Für wirklich komplexe Apps mit Middleware, Time-Travel-Debugging oder vielen unabhängigen Slices ziehe Redux Toolkit oder Zustand in Betracht. Aber für die meisten Apps sind useReducer + Context ausreichend und haben null Dependencies.

react
const CartContext = createContext();

function cartReducer(state, action) {
  switch (action.type) {
    case "ADD":
      const existing = state.find((i) => i.id === action.item.id);
      if (existing) {
        return state.map((i) =>
          i.id === action.item.id ? { ...i, qty: i.qty + 1 } : i
        );
      }
      return [...state, { ...action.item, qty: 1 }];
    case "REMOVE":
      return state.filter((i) => i.id !== action.id);
    case "CLEAR":
      return [];
    default:
      return state;
  }
}

function CartProvider({ children }) {
  const [items, dispatch] = useReducer(cartReducer, []);
  const value = useMemo(() => ({
    items,
    total: items.reduce((s, i) => s + i.price * i.qty, 0),
    addItem: (item) => dispatch({ type: "ADD", item }),
    removeItem: (id) => dispatch({ type: "REMOVE", id }),
  }), [items]);
  return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}

Redux Toolkit-Grundlagen

Redux Toolkit (RTK) ist die moderne, empfohlene Methode, Redux zu verwenden. createSlice auto-generiert Action-Creators und Reducer. Es verwendet intern Immer, sodass du State direkt 'mutierst' (state.value += 1) und Immer das unveränderliche Update erzeugt. configureStore richtet den Store mit sinnvollen Defaults ein (Redux DevTools, Thunk-Middleware). useSelector liest State; useDispatch dispatcht Actions. RTK eliminiert Redux-Boilerplate (keine switch-Statements, keine Action-Type-Konstanten). Für asynchrone Logik verwende createAsyncThunk. RTK Query (enthalten) behandelt Daten-Abruf und Caching.

react
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { useSelector, useDispatch } from "react-redux";

const counterSlice = createSlice({
  name: "counter",
  initialState: { value: 0 },
  reducers: {
    increment: (state) => { state.value += 1; }, // Immer: mutate safely
    decrement: (state) => { state.value -= 1; },
    addBy: (state, action) => { state.value += action.payload; },
  },
});

const store = configureStore({
  reducer: { counter: counterSlice.reducer },
});

export const { increment, decrement, addBy } = counterSlice.actions;

// Usage in component
function Counter() {
  const count = useSelector((state) => state.counter.value);
  const dispatch = useDispatch();
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(addBy(5))}>+5</button>
    </div>
  );
}

Zustand (leichtgewichtige Alternative)

Zustand ist eine minimale State-Management-Bibliothek — keine Provider, keine Boilerplate. Erstelle einen Store mit create(), greife via Hooks mit Selektor-Funktionen zu. Selektoren verhindern Re-Renders: nur Komponenten, die den geänderten Slice verwenden, re-rendern. Dies löst Contexts Re-Render-Problem ohne Reduxs Komplexität. Für Objekt-Selektoren (die {a, b} zurückgeben) verwende flachen Vergleich, um unnötige Re-Renders zu verhindern. Zustand unterstützt Middleware (persist, devtools, immer). Es ist ideal für kleine bis mittelgroße Apps, wo Redux überdimensioniert ist, aber Context zu viele Re-Renders verursacht. Die API ist winzig, aber mächtig.

react
import { create } from "zustand";

const useStore = create((set, get) => ({
  count: 0,
  user: null,
  increment: () => set((state) => ({ count: state.count + 1 })),
  setUser: (user) => set({ user }),
  reset: () => set({ count: 0, user: null }),
  // Access other state with get()
  doubleCount: () => get().count * 2,
}));

// Usage: select only what you need (prevents re-renders)
function Counter() {
  const count = useStore((state) => state.count);
  const increment = useStore((state) => state.increment);
  return <button onClick={increment}>{count}</button>;
}

// Multiple selections
function Profile() {
  const { user, setUser } = useStore(
    (state) => ({ user: state.user, setUser: state.setUser })
  );
  // Use shallow comparison for object selectors
  // import { shallow } from "zustand/shallow";
  // useStore(selector, shallow);
}

React Query (Server-State)

React Query (TanStack Query) verwaltet Server-State — Daten, die von APIs abgerufen werden. Es behandelt Caching, Background-Refetching, veraltete Daten, optimistische Updates und Paginierung automatisch. queryKey identifiziert gecachte Daten (wie ein Cache-Key). staleTime steuert, wie lange Daten als frisch gelten. invalidateQueries nach Mutations refetcht abhängige Queries. Im Gegensatz zu Redux (Client-State) ist React Query speziell für asynchrone Server-Daten gebaut. Es eliminiert manuelle Loading/Error-States, useEffect-Fetching und Cache-Management. Für die meisten Apps ersetzen React Query + lokaler State (useState/useReducer) Redux vollständig.

react
import { useQuery, useMutation, QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Users />
    </QueryClientProvider>
  );
}

function Users() {
  const { data, isLoading, error, refetch } = useQuery({
    queryKey: ["users"],
    queryFn: () => fetch("/api/users").then((r) => r.json()),
    staleTime: 60000, // fresh for 60s
    refetchOnWindowFocus: true,
  });

  const mutation = useMutation({
    mutationFn: (newUser) =>
      fetch("/api/users", { method: "POST", body: JSON.stringify(newUser) }),
    onSuccess: () => queryClient.invalidateQueries(["users"]),
  });

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  return (
    <div>
      {data.map((u) => <div key={u.id}>{u.name}</div>)}
      <button onClick={() => mutation.mutate({ name: "New" })}>Add</button>
    </div>
  );
}
16

Testing (React Testing Library)

Basis-Komponenten-Test

React Testing Library (RTL) testet Komponenten so, wie Nutzer mit ihnen interagieren — nach Role, Label und Text, nicht nach Implementierungsdetails. getByRole ist die bevorzugte Query (testet auch Accessibility). userEvent simuliert echte Nutzer-Interaktionen (Tippen, Klicken) genauer als fireEvent. Tests sollten internen State nicht testen; stattdessen sichtbare Ausgabe und Verhalten verifizieren. Wenn du nicht nach Role queryen kannst, verwende getByLabelText, getByText oder getByDisplayValue. Vermeide getByTestId, außer wenn nötig. Dieser Ansatz macht Tests resistent gegen Refactoring — sie testen, was Nutzer sehen und tun.

react
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Counter } from "./Counter";

test("counter increments on click", async () => {
  const user = userEvent.setup();
  render(<Counter />);

  // Find by accessible role (not test-id)
  expect(screen.getByRole("heading")).toHaveTextContent("0");

  await user.click(screen.getByRole("button", { name: /increment/i }));

  expect(screen.getByRole("heading")).toHaveTextContent("1");
});

test("displays error for invalid input", async () => {
  const user = userEvent.setup();
  render(<Form />);

  await user.type(screen.getByLabelText(/email/i), "not-an-email");
  await user.click(screen.getByRole("button", { name: /submit/i }));

  expect(screen.getByRole("alert")).toHaveTextContent(/invalid email/i);
});

Hooks testen

renderHook testet Custom Hooks isoliert. result.current hält den Rückgabewert des Hooks. Alle State-Updates müssen in act() gewickelt werden, um sicherzustellen, dass React sie synchron verarbeitet. rerender lässt dich Effects testen, die von sich ändernden Props abhängen. Für asynchrone Hooks (useEffect mit Fetch) verwende waitFor oder findBy-Queries (die auf Updates warten). Hooks direkt zu testen ist schneller und fokussierter als das Testen durch eine Komponente. Teste Hooks jedoch auch durch Komponenten-Integrationstests, um die reale Verwendung zu verifizieren. renderHook ist in @testing-library/react v13+ verfügbar.

react
import { renderHook, act } from "@testing-library/react";
import { useCounter } from "./useCounter";

test("useCounter increments and decrements", () => {
  const { result } = renderHook(() => useCounter(0));

  expect(result.current.count).toBe(0);

  // Wrap state updates in act()
  act(() => result.current.increment());
  expect(result.current.count).toBe(1);

  act(() => result.current.decrement());
  expect(result.current.count).toBe(0);

  act(() => result.current.reset());
  expect(result.current.count).toBe(0);
});

// Testing with initial props that change
test("useEffect runs on dependency change", () => {
  const { result, rerender } = renderHook(
    ({ id }) => useFetchUser(id),
    { initialProps: { id: 1 } }
  );
  rerender({ id: 2 });
  // Effect re-ran with new id
});

Async testen & Mocking

MSW (Mock Service Worker) fängt Netzwerk-Requests auf Service-Worker-Ebene ab — Tests verwenden echtes fetch(), erhalten aber gemockte Responses. Dies ist realistischer als das direkte Mocken von fetch. setupServer für Node (Jest), setupWorker für Browser. beforeAll/afterAll-Lifecycle verwaltet den Server. server.use() überschreibt Handler pro Test. findBy-Queries (async) warten auf das Erscheinen von Elementen — verwende für asynchrones Rendering. queryBy gibt null zurück, wenn nicht gefunden (zum Behaupten von Abwesenheit). waitFor pollt nach einer Bedingung. MSW kann auch für Development-Mocking und Storybook verwendet werden.

react
import { render, screen, waitFor } from "@testing-library/react";
import { rest } from "msw";
import { setupServer } from "msw/node";
import { UserProfile } from "./UserProfile";

// Mock API with MSW (Mock Service Worker)
const server = setupServer(
  rest.get("/api/users/:id", (req, res, ctx) => {
    return res(ctx.json({ id: 1, name: "Alice" }));
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test("displays user after fetch", async () => {
  render(<UserProfile id={1} />);

  // findBy waits for async update
  expect(await screen.findByText("Alice")).toBeInTheDocument();
  expect(screen.queryByText("Loading")).not.toBeInTheDocument();
});

test("shows error on fetch failure", async () => {
  server.use(
    rest.get("/api/users/:id", (req, res, ctx) =>
      res(ctx.status(500))
    )
  );
  render(<UserProfile id={1} />);
  expect(await screen.findByText(/error/i)).toBeInTheDocument();
});

Context & Provider testen

Erstelle ein Custom Render-Utility, das Komponenten mit erforderlichen Providern (Theme, Auth, Router usw.) umwickelt. Dies vermeidet die Wiederholung des Provider-Setups in jedem Test. Re-exportiere RTL-Funktionen aus deiner test-utils-Datei, sodass Tests von dort importieren. Für Router-Testing verwende MemoryRouter (nicht BrowserRouter) mit initialEntries, um die Start-URL zu setzen — keine echte Browser-History nötig. Für Redux wickle in einen Test-Provider mit einem echten oder Mock-Store ein. Dieses Muster hält Tests sauber und stellt sicher, dass alle Komponenten ihren erforderlichen Context haben. Es ist das Standard-Setup für jede React-Testing-Infrastruktur.

react
// Custom render that wraps with providers
import { render } from "@testing-library/react";
import { ThemeProvider } from "./ThemeProvider";

function customRender(ui, { theme = "light", ...options } = {}) {
  function Wrapper({ children }) {
    return <ThemeProvider initialTheme={theme}>{children}</ThemeProvider>;
  }
  return render(ui, { wrapper: Wrapper, ...options });
}

// Re-export everything
export * from "@testing-library/react";
export { customRender as render };

// In test files, import from your test-utils:
// import { render, screen } from "../test-utils";

test("button uses theme color", () => {
  customRender(<Button>Click</Button>, { theme: "dark" });
  expect(screen.getByRole("button")).toHaveClass("btn-dark");
});

// Testing with router
import { MemoryRouter } from "react-router-dom";
render(
  <MemoryRouter initialEntries={["/users/123"]}>
    <App />
  </MemoryRouter>
);

Events & Interaktionen testen

userEvent.setup() erzeugt eine Nutzer-Instanz für realistische Interaktionen: type (Zeichen für Zeichen), click, tab, keyboard (mit Key-Codes wie {Escape}, {Enter}), selectOptions, upload und mehr. Await immer Nutzer-Interaktionen — sie sind async. Tastatur-Navigation zu testen ist entscheidend für Accessibility. Für Form-Testing fülle alle Felder und verifiziere, dass onSubmit die korrekten Daten empfängt. userEvent wird fireEvent vorgezogen, weil es echtes Browser-Verhalten simuliert (Focus, Blur, Input-Events in korrekter Reihenfolge). Teste den vollen Nutzer-Flow, nicht einzelne Event-Handler.

react
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

test("form submission flow", async () => {
  const onSubmit = jest.fn();
  const user = userEvent.setup();
  render(<LoginForm onSubmit={onSubmit} />);

  // Fill form fields
  await user.type(screen.getByLabelText(/email/i), "[email protected]");
  await user.type(screen.getByLabelText(/password/i), "password123");

  // Submit
  await user.click(screen.getByRole("button", { name: /login/i }));

  expect(onSubmit).toHaveBeenCalledWith({
    email: "[email protected]",
    password: "password123",
  });
});

test("keyboard navigation", async () => {
  const user = userEvent.setup();
  render(<Modal />);
  await user.tab(); // focus first element
  expect(screen.getByRole("button", { name: /close/i })).toHaveFocus();
  await user.keyboard("{Escape}"); // press Escape
  expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
17

TypeScript + React

Komponenten-Props typen

TypeScript mit React bietet Typsicherheit für Props. Verwende Interfaces oder Type-Aliases für Props. Optionale Props verwenden ?. Union-Types (variant) schränken Werte ein. React.ReactNode akzeptiert jeden renderbaren Content (Strings, Elemente, Arrays). Das Erweitern von HTML-Attributen (React.InputHTMLAttributes) lässt deine Komponente alle nativen Attribute (placeholder, onChange usw.) akzeptieren, während Custom-Props hinzugefügt werden. Der Spread {...rest} übergibt verbleibende Attribute an das native Element. Dieses Muster erstellt typsichere, flexible Komponenten. Exportiere Prop-Typen immer, sodass Consumer sie referenzieren können.

react
// Basic props
interface ButtonProps {
  text: string;
  onClick: () => void;
  variant?: "primary" | "secondary"; // union type
  disabled?: boolean;
}

function Button({ text, onClick, variant = "primary", disabled }: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant}`}
      onClick={onClick}
      disabled={disabled}
    >
      {text}
    </button>
  );
}

// Children prop
interface CardProps {
  title: string;
  children: React.ReactNode; // any renderable content
}

// Extending HTML attributes
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label: string;
  error?: string;
}

function Input({ label, error, ...rest }: InputProps) {
  return (
    <label>
      {label}
      <input {...rest} />
      {error && <span className="error">{error}</span>}
    </label>
  );
}

Hooks mit TypeScript

TypeScript fügt Hooks Typsicherheit hinzu. useState<T> spezifiziert den State-Typ; useState<T | null>(null) für nullable State. useRef<T>(null) typet die ref — current ist T | null. Für useContext definiere einen Context-Typ und wirf, wenn undefined (sodass Consumer den non-undefined-Typ erhalten). Für useReducer typet die Action als discriminated union — der switch auf action.type engt den Typ in jedem case ein und gibt dir typsicheren Payload-Zugriff. Diese Muster eliminieren Laufzeitfehler durch undefined-Zugriff und inkorrekte Action-Payloads.

react
// useState with types
const [count, setCount] = useState<number>(0);
const [user, setUser] = useState<User | null>(null);
const [items, setItems] = useState<string[]>([]);

// useRef
const inputRef = useRef<HTMLInputElement>(null);
// Access: inputRef.current?.focus()

// useContext
interface ThemeContextType {
  theme: "light" | "dark";
  toggle: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error("useTheme must be inside ThemeProvider");
  return ctx; // ctx is now ThemeContextType, not undefined
}

// useReducer
type Action = { type: "increment" } | { type: "set"; value: number };
const [state, dispatch] = useReducer((state: number, action: Action) => {
  switch (action.type) {
    case "increment": return state + 1;
    case "set": return action.value;
  }
}, 0);

Generische Komponenten

Generische Komponenten und Hooks arbeiten mit jedem Datentyp, während die Typsicherheit erhalten bleibt. Der <T>-Typparameter wird aus der items-Prop inferiert, sodass renderItem und keyExtractor automatisch den korrekten Typ erhalten. So recreiert TypeScript generische Utility-Komponenten (List, Table, Select) mit voller Typsicherheit. Generische Hooks (useArray<T>) erhalten Typen ähnlich durch Operationen. Der Schlüssel: TypeScript leitet T aus der Verwendung ab, sodass Consumer es selten explizit spezifizieren müssen. Dieses Muster ist essenziell für den Bau wiederverwendbarer, typsicherer Komponenten-Bibliotheken.

react
// Generic component: works with any type
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={keyExtractor(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// Usage: TypeScript infers T from items
<List
  items={[{ id: "1", name: "Alice" }, { id: "2", name: "Bob" }]}
  renderItem={(user) => <span>{user.name}</span>}
  keyExtractor={(user) => user.id}
/>

// Generic hook
function useArray<T>(initial: T[]) {
  const [array, setArray] = useState(initial);
  const push = (item: T) => setArray((prev) => [...prev, item]);
  return { array, push, setArray };
}

Event-Typen & Refs

React-Event-Typen sind spezifisch: ChangeEvent für Inputs, FormEvent für Formulare, MouseEvent für Clicks. Jeder ist generisch über den Element-Typ (e.target ist korrekt getypt). forwardRef mit TypeScript erfordert zwei Typparameter: den ref-Typ und den Props-Typ. forwardRef wird benötigt, wenn eine Komponente eine ref an ein DOM-Element freilegen muss (für Focus, Messung usw.). Setze immer displayName für forwardRef/memo-Komponenten für besseres DevTools-Debugging. React 19 erlaubt ref als reguläre Prop, was die Notwendigkeit für forwardRef reduziert, aber es ist noch in bestehendem Code verbreitet.

react
// Event types
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
  console.log(e.target.value); // string
}

function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
  e.preventDefault();
  const formData = new FormData(e.currentTarget);
}

function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
  console.log(e.clientX, e.clientY);
}

// forwardRef with TypeScript
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label: string;
}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ label, ...props }, ref) => (
    <label>
      {label}
      <input ref={ref} {...props} />
    </label>
  )
);
Input.displayName = "Input";

// Usage: const ref = useRef<HTMLInputElement>(null);
// <Input ref={ref} label="Email" />

Utility-Types für Props

TypeScript-Utility-Types sind mächtig für Prop-Komposition. Pick wählt spezifische Props (für Subset-Komponenten). Omit schließt Props aus (für Verhaltens-Ersetzung). Partial macht alle Props optional (für Default-Prop-Muster). ComponentProps<typeof Component> extrahiert den Prop-Typ einer Komponente — nützlich zum Wrappen/Erweitern bestehender Komponenten. Record<K, V> erstellt ein Type-Mapping von Keys zu Werten (ideal für Variant-to-Class-Maps). Diese Utilities ermöglichen DRY, typsichere Prop-Definitionen ohne Wiederholung von Interfaces. Meistere sie, um wartbares React + TypeScript zu schreiben.

react
// Pick: select specific props
interface ButtonProps {
  text: string;
  onClick: () => void;
  color: string;
  size: "sm" | "md" | "lg";
}
type IconButtonProps = Pick<ButtonProps, "onClick" | "size"> & {
  icon: React.ReactNode;
};

// Omit: exclude specific props
type LinkButtonProps = Omit<ButtonProps, "onClick"> & {
  href: string;
};

// Partial: all props optional (for defaults)
type DefaultProps = Partial<ButtonProps>;

// ComponentProps: extract props from existing component
type MyButtonProps = React.ComponentProps<typeof Button> & {
  variant?: "custom";
};

// ReturnType: type of a function's return
type User = ReturnType<typeof fetchUser>;

// Record for prop maps
type ButtonVariants = Record<"primary" | "danger" | "ghost", string>;
18

Concurrent Features (useTransition, useDeferredValue)

useTransition

useTransition markiert ein State-Update als nicht-dringend (Transition). Dringende Updates (Input-Wert) rendern sofort für Responsivität; nicht-dringende Updates (Filtern von 10.000 Items) können unterbrochen werden, wenn der Nutzer erneut tippt. isPending zeigt an, dass die Transition läuft (zeige einen dezenten Loading-Indikator). Dies verhindert, dass die UI während teurer Renders einfriert. Der Schlüssel: React kann veraltete Transitions unterbrechen und verwerfen, was die UI responsiv hält. Verwende für Search-Filtering, Tab-Switching und jedes State-Update, das schweres Rendering auslöst. Wickele keine dringenden Updates (Tippen, Klicken) in Transitions ein.

react
import { useTransition, useState } from "react";

function SearchResults() {
  const [isPending, startTransition] = useTransition();
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);

  const handleSearch = (value) => {
    setQuery(value); // urgent: update input immediately
    startTransition(() => {
      // non-urgent: heavy filtering can be interrupted
      const filtered = heavyFilter(allItems, value);
      setResults(filtered);
    });
  };

  return (
    <div>
      <input value={query} onChange={(e) => handleSearch(e.target.value)} />
      {isPending && <span>Updating...</span>}
      <ul>{results.map((r) => <li key={r.id}>{r.name}</li>)}</ul>
    </div>
  );
}

useDeferredValue

useDeferredValue ist das deklarative Gegenstück zu useTransition. Es gibt eine verzögerte Kopie eines Werts zurück, die mit niedrigerer Priorität aktualisiert wird. Der Input aktualisiert sich sofort (dringend); die teure Liste re-rendert mit dem verzögerten Wert (nicht-dringend). React.memo auf der Results-Komponente ist entscheidend — es verhindert Re-Rendering bei jedem Tastenanschlag, nur wenn sich deferredQuery ändert. isStale (Vergleich von current vs. deferred) lässt dich einen visuellen Indikator zeigen (abgedunkelt, Spinner). Verwende useDeferredValue, wenn du das State-Update nicht kontrollieren kannst (z. B. Wert kommt aus Props). Verwende useTransition, wenn du das Update kontrollierst.

react
function Search() {
  const [query, setQuery] = useState("");
  // query updates immediately; deferredQuery lags behind
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <Results query={deferredQuery} isStale={isStale} />
    </div>
  );
}

// Memoize the expensive component so it only re-renders
// when deferredQuery changes (not on every keystroke)
const Results = React.memo(function Results({ query, isStale }) {
  const items = expensiveSearch(query); // heavy computation
  return (
    <div style={{ opacity: isStale ? 0.5 : 1 }}>
      {items.map((i) => <div key={i.id}>{i.name}</div>)}
    </div>
  );
});

useOptimistic (React 19)

useOptimistic (React 19) implementiert optimistische Updates — die UI aktualisiert sich sofort mit dem erwarteten Ergebnis und reconciliert dann mit der tatsächlichen Server-Response. Der optimistische State zeigt während der asynchronen Operation; wenn die echten Daten ankommen (Komponente re-rendert mit neuen Props), wird der optimistische Wert automatisch ersetzt. Wenn die Operation fehlschlägt, wird das optimistische Update einfach beim nächsten Render mit unveränderten Props zurückgesetzt. Dies eliminiert manuelle optimistische Update-Logik (Tracking von Pending-State, Zurücksetzen bei Fehler). Markiere ausstehende Items (pending: true), um Loading-Indikatoren zu zeigen. Perfekt für Likes, Kommentare und Toggles.

react
import { useOptimistic } from "react";

function ThumbsUp({ likes, addLike }) {
  // Optimistic state: updates immediately, reverts on error
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    likes,
    (state, newLike) => [...state, newLike]
  );

  const handleClick = async () => {
    const newLike = { id: Date.now(), pending: true };
    addOptimisticLike(newLike); // instant UI update
    try {
      await addLike(newLike); // actual API call
    } catch {
      // Reverts automatically on re-render with real data
    }
  };

  return (
    <div>
      <button onClick={handleClick}>👍 {optimisticLikes.length}</button>
      {optimisticLikes.some((l) => l.pending) && <span>Saving...</span>}
    </div>
  );
}

use (React 19-Hook)

use() ist React 19s neuer Hook, der Context oder Promises liest. Im Gegensatz zu useContext kann use() bedingt aufgerufen werden (innerhalb von if-Statements, Schleifen) — es hat nicht die Rules-of-Hooks-Einschränkung. Für Promises suspendiert use() die Komponente, bis das Promise resolved (erfordert eine Suspense-Grenze). Das Promise wird im Eltern-Element erstellt und als Prop übergeben — dies startet das Fetchen während des Renders (nicht in useEffect), wodurch Waterfalls früher starten können. Dasselbe Promise kann an mehrere Komponenten übergeben werden (Deduplikation). use() überbrückt die Lücke zwischen synchronem Context und asynchronen Daten.

react
import { use } from "react";

// Read context with use (works in conditions!)
function Theme() {
  // Unlike useContext, use() can be inside conditions
  if (showTheme) {
    const theme = use(ThemeContext); // conditional context!
    return <div style={{ background: theme.color }} />;
  }
  return null;
}

// Read promises with use (Suspense integration)
function UserProfile({ userPromise }) {
  // Suspends until promise resolves
  const user = use(userPromise);
  return <h1>{user.name}</h1>;
}

// Parent passes promise (starts fetching during render)
function App() {
  const userPromise = fetchUser(); // starts immediately
  return (
    <Suspense fallback={<Loading />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

Concurrent-Rendering-Muster

Concurrent-Muster: useTransition für nicht-dringende Tab-Switches (hält die Nav responsiv, während schwerer Content rendert). useSyncExternalStore abonniert sicher externe Stores (Browser-APIs, Redux, Zustand) im Concurrent-Modus — es bietet eine Snapshot-Funktion für Client und Server (SSR-safe). Verwende niemals externen veränderbaren State direkt in Render (Tearing-Risiko); gehe immer durch useSyncExternalStore. Die drei Argumente: subscribe (gibt Cleanup zurück), getSnapshot (aktueller Wert), getServerSnapshot (SSR-Initialwert). Dies stellt konsistente Reads während Concurrent-Rendering sicher. Bibliotheken wie Redux und Zustand verwenden dies intern.

react
// Pattern 1: Deferred search with transition
function App() {
  const [tab, setTab] = useState("home");
  const [isPending, startTransition] = useTransition();

  return (
    <>
      <nav>
        <button
          onClick={() => startTransition(() => setTab("analytics"))}
          disabled={isPending}
        >
          {isPending ? "Loading..." : "Analytics"}
        </button>
      </nav>
      {tab === "home" && <Home />}
      {tab === "analytics" && <HeavyAnalytics />}
    </>
  );
}

// Pattern 2: useSyncExternalStore for external state
function useOnlineStatus() {
  return useSyncExternalStore(
    (callback) => {
      window.addEventListener("online", callback);
      window.addEventListener("offline", callback);
      return () => {
        window.removeEventListener("online", callback);
        window.removeEventListener("offline", callback);
      };
    },
    () => navigator.onLine, // client snapshot
    () => true // server snapshot (SSR)
  );
}
19

Context API Deep Dive

createContext & Provider

createContext erstellt ein Context-Objekt mit einem Default-Wert, der verwendet wird, wenn kein Provider gefunden wird. Die value-Prop des Providers wird von allen Nachfahren konsumiert. Wickele den Wert in useCallback/useMemo ein, um unnötige Re-Renders zu verhindern.

react
const ThemeContext = React.createContext({ theme: 'light', toggle: () => {} });

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const toggle = useCallback(() => setTheme((t) => (t === 'light' ? 'dark' : 'light')), []);
  return (
    <ThemeContext.Provider value={{ theme, toggle }}>
      {children}
    </ThemeContext.Provider>
  );
}

useContext

useContext liest den nächsten Provider-Wert und re-rendert die Komponente, wenn sich dieser Wert ändert. Verschachtele mehrere Provider für verschiedene Belange. Teile Contexts nach Update-Frequenz für Performance.

react
function ThemedButton() {
  const { theme, toggle } = useContext(ThemeContext);
  return (
    <button onClick={toggle} style={{ background: theme === 'dark' ? '#333' : '#eee' }}>
      Toggle Theme
    </button>
  );
}

Context mit Reducer

Die Paarung von useReducer mit Context erstellt einen globalen Store ohne Redux. Der Reducer zentralisiert State-Logik; der Context verteilt State und dispatch. Consumer können Actions dispatchen, ohne Prop-Drilling.

react
const StoreContext = React.createContext(null);

function storeReducer(state, action) {
  switch (action.type) {
    case 'add': return { items: [...state.items, action.item] };
    case 'remove': return { items: state.items.filter((_, i) => i !== action.index) };
    default: return state;
  }
}

function StoreProvider({ children }) {
  const [state, dispatch] = useReducer(storeReducer, { items: [] });
  return <StoreContext.Provider value={{ state, dispatch }}>{children}</StoreContext.Provider>;
}

Context-Renders optimieren

Wenn State und dispatch im selben Context leben, re-rendern bei jeder State-Änderung alle Consumer. Sie zu trennen bedeutet, dass dispatch-only-Komponenten bei State-Änderungen nie re-rendern. dispatch aus useReducer ist stabil.

react
const StateContext = React.createContext(null);
const DispatchContext = React.createContext(null);

function Provider({ children }) {
  const [state, dispatch] = useReducer(reducer, initial);
  return (
    <StateContext.Provider value={state}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  );
}

Custom Hook für Context

Das Einwickeln von useContext in einen Custom Hook gibt eine saubere API und einen klaren Fehler, wenn der Provider fehlt. Exportiere sowohl den Provider als auch den Hook. Dies ist die empfohlene Methode, Context zu konsumieren.

react
export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth must be used within AuthProvider');
  return ctx;
}
20

useReducer

Basis-useReducer

useReducer ist eine Alternative zu useState für komplexe State-Logik. Der Reducer ist eine reine Funktion: (state, action) => newState. dispatch ist stabil, sodass du ihn weiterreichen kannst, ohne dir über Re-Renders Gedanken zu machen.

react
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: throw new Error('Unknown action: ' + action.type);
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  return <button onClick={() => dispatch({ type: 'increment' })}>{state.count}</button>;
}

Lazy Initialization

Das dritte Argument an useReducer ist eine init-Funktion, die einmal während des initialen Renders läuft. Dies ist nützlich, wenn der initiale State teuer zu berechnen ist oder wenn reset zu einem berechneten State zurückkehren soll.

react
function init(initialCount) {
  return { count: initialCount, history: [] };
}

function reducer(state, action) {
  switch (action.type) {
    case 'increment': return { ...state, count: state.count + 1 };
    case 'reset':     return init(action.payload);
    default: return state;
  }
}

const [state, dispatch] = useReducer(reducer, initialCount, init);

Komplexe State-Form

useReducer glänzt, wenn State mehrere verwandte Felder hat. Jede Action beschreibt einen vollständigen State-Übergang, was die Logik leichter nachvollziehbar macht als verstreute setState-Aufrufe. Halte den Reducer rein.

react
const initialState = { users: [], loading: false, error: null, filter: 'all' };

function reducer(state, action) {
  switch (action.type) {
    case 'fetch-start': return { ...state, loading: true, error: null };
    case 'fetch-success': return { ...state, loading: false, users: action.users };
    case 'fetch-error': return { ...state, loading: false, error: action.error };
    case 'set-filter': return { ...state, filter: action.filter };
    default: return state;
  }
}

Reducer mit Context

Die Kombination von useReducer mit Context erzeugt einen leichtgewichtigen Redux-ähnlichen Store. Der Reducer hält die Logik; der Context verteilt State und dispatch. Dies ist das empfohlene Muster für App-weiten State in mittelgroßen Apps.

react
function todoReducer(state, action) {
  switch (action.type) {
    case 'add': return [...state, { id: Date.now(), text: action.text, done: false }];
    case 'toggle': return state.map((t) => t.id === action.id ? { ...t, done: !t.done } : t);
    case 'delete': return state.filter((t) => t.id !== action.id);
    default: return state;
  }
}

export function TodoProvider({ children }) {
  const [todos, dispatch] = useReducer(todoReducer, []);
  return <TodoContext.Provider value={{ todos, dispatch }}>{children}</TodoContext.Provider>;
}

Action-Typen & Muster

Definiere Action-Typen als Konstanten, um Tippfehler zu vermeiden und IDE-Autocomplete zu aktivieren. Die Action-Form { type, payload? } ist eine häufige Konvention. Für TypeScript definiere eine discriminated union von Action-Typen.

react
const ACTIONS = { ADD: 'add', UPDATE: 'update', DELETE: 'delete' };

function reducer(state, action) {
  switch (action.type) {
    case ACTIONS.ADD:
      return [...state, { id: action.id, ...action.payload }];
    case ACTIONS.UPDATE:
      return state.map((item) => item.id === action.id ? { ...item, ...action.payload } : item);
    case ACTIONS.DELETE:
      return state.filter((item) => item.id !== action.id);
    default: return state;
  }
}
21

useMemo & useCallback

useMemo

useMemo cacht das Ergebnis einer Berechnung und berechnet nur neu, wenn sich Dependencies ändern. Verwende es für teure Berechnungen (Sortieren, Filtern großer Arrays). Das Dependency-Array muss alles enthalten, was der Callback verwendet.

react
function ProductList({ products, filter }) {
  const filtered = useMemo(() => {
    return products.filter((p) => p.category === filter);
  }, [products, filter]);

  const sorted = useMemo(() => [...filtered].sort((a, b) => a.price - b.price), [filtered]);
  return <ul>{sorted.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}

useCallback

useCallback memoisiert eine Funktion, sodass sie über Renders hinweg dieselbe Identität behält, außer wenn sich Dependencies ändern. Dies ist kritisch, wenn Callbacks an memoisierte Kinder übergeben werden — ohne es re-rendert das Kind jedes Mal.

react
function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = useCallback(() => setCount((c) => c + 1), []);
  return <MemoizedChild onClick={handleClick} />;
}

React.memo

React.memo wickelt eine Komponente ein, sodass sie nur re-rendert, wenn sich ihre Props ändern (flacher Vergleich). Das zweite Argument ist ein Custom-Comparator, der true zurückgibt, um Re-Rendering zu überspringen. Kombiniere memo mit useCallback/useMemo für Props.

react
const ExpensiveItem = React.memo(function ExpensiveItem({ value, onClick }) {
  return <li onClick={onClick}>{value}</li>;
});

// With custom comparison
const DeepChild = React.memo(
  ({ user }) => <div>{user.name}</div>,
  (prev, next) => prev.user.id === next.user.id
);

Wann memoisieren

Memoisierung hat Kosten, die die Einsparungen übersteigen können. Memoisiere nur, wenn: (1) die Berechnung teuer ist, (2) der Wert an ein memoisiertes Kind übergeben wird, oder (3) der Wert als Dependency in useEffect/useMemo verwendet wird.

react
// GOOD: expensive computation
const sorted = useMemo(() => heavySort(data), [data]);

// GOOD: callback passed to memoized child
const onSelect = useCallback((id) => setSelected(id), []);

// BAD: cheap operation, no perf issue
const label = useMemo(() => first + ' ' + last, [first, last]);

useMemo für referenzielle Gleichheit

useMemo stellt sicher, dass Objekte und Arrays über Renders hinweg dieselbe Referenz behalten, was wichtig ist, wenn sie als Dependencies in useEffect/useMemo/useCallback verwendet werden. Ohne es erzeugt { q: query } bei jedem Render ein neues Objekt.

react
function Search({ query }) {
  const params = useMemo(() => ({ q: query, limit: 10 }), [query]);
  useEffect(() => {
    api.search(params).then(setData);
  }, [params]); // Without useMemo, this fires every render
}
22

Portals & Refs

createPortal

createPortal rendert Children in einen DOM-Knoten außerhalb des DOM-Baums der Eltern-Komponente (üblicherweise document.body). Dies ist essenziell für Modals, Tooltips und Dropdowns, die Eltern-Stacking-Contexts entkommen müssen.

react
import { createPortal } from 'react-dom';

function Modal({ open, onClose, children }) {
  if (!open) return null;
  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()}>{children}</div>
    </div>,
    document.body
  );
}

useRef

useRef gibt ein veränderbares Objekt zurück, dessen .current über Renders hinweg persists, ohne Re-Renders auszulösen. Die primäre Verwendung ist der Zugriff auf DOM-Knoten. Es speichert auch veränderbare Werte, die die UI nicht beeinflussen.

react
function FocusInput() {
  const inputRef = useRef(null);
  const focus = () => inputRef.current?.focus();
  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={focus}>Focus</button>
    </>
  );
}

forwardRef

forwardRef lässt eine Eltern-Komponente eine ref durch eine Wrapper-Komponente an einen Kind-DOM-Knoten weiterreichen. Ohne es verbietet React das Weiterreichen von ref als Prop. Die ref ist das zweite Argument der gewickelten Funktion.

react
const FancyInput = React.forwardRef(function FancyInput({ label, ...props }, ref) {
  return (
    <label>{label}<input ref={ref} {...props} className="fancy-input" /></label>
  );
});

useImperativeHandle

useImperativeHandle passt die Instanz an, die dem Eltern-Element via ref freigelegt wird — statt des rohen DOM-Knotens sieht das Eltern-Element nur die Methoden, die du definierst. Verwende sparsam; bevorzuge deklarative Props, wenn möglich.

react
const VideoPlayer = React.forwardRef(function VideoPlayer(props, ref) {
  const videoRef = useRef(null);
  useImperativeHandle(ref, () => ({
    play: () => videoRef.current?.play(),
    pause: () => videoRef.current?.pause(),
    seek: (time) => { if (videoRef.current) videoRef.current.currentTime = time; },
  }));
  return <video ref={videoRef} src={props.src} />;
});

Refs für veränderbare Werte

useRef speichert veränderbare Werte, die keine Re-Renders auslösen sollten — wie Timer-IDs, WebSocket-Instanzen oder "is mounted"-Flags. Räume Side Effects immer in der useEffect-Cleanup-Funktion auf.

react
function Stopwatch() {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef(null);
  const start = () => {
    if (intervalRef.current) return;
    intervalRef.current = setInterval(() => setSeconds((s) => s + 1), 1000);
  };
  const stop = () => { clearInterval(intervalRef.current); intervalRef.current = null; };
  useEffect(() => () => clearInterval(intervalRef.current), []);
  return <>{seconds}<button onClick={start}>Start</button><button onClick={stop}>Stop</button></>;
}
23

Error Boundaries

Klassen-Error-Boundary

Error Boundaries sind Klassen-Komponenten, die Fehler im Kind-Komponenten-Baum während des Renderns abfangen. getDerivedStateFromError aktualisiert State, um den Fallback zu rendern; componentDidCatch protokolliert den Fehler. Sie fangen KEINE Fehler in Event-Handlern oder asynchronem Code ab.

react
class ErrorBoundary extends React.Component {
  constructor(props) { super(props); this.state = { hasError: false, error: null }; }
  static getDerivedStateFromError(error) { return { hasError: true, error }; }
  componentDidCatch(error, info) { console.error('Caught:', error, info); }
  render() {
    if (this.state.hasError) return this.props.fallback || <h1>Something went wrong.</h1>;
    return this.props.children;
  }
}

Error Boundaries verwenden

Platziere Error Boundaries strategisch, sodass ein Absturz in einem Teil der UI nicht die ganze App mitreißt. Granulare Grenzen um Widgets lassen den Rest der App weiterlaufen.

react
function App() {
  return (
    <ErrorBoundary fallback={<ErrorPage />}>
      <Header />
      <ErrorBoundary fallback={<SidebarCrash />}><Sidebar /></ErrorBoundary>
      <ErrorBoundary fallback={<ContentCrash />}><MainContent /></ErrorBoundary>
    </ErrorBoundary>
  );
}

Error-State zurücksetzen

Error Boundaries bleiben im Error-State, bis sich ihr State ändert. Biete einen "Try again"-Button, der hasError auf false zurücksetzt und React die Kinder neu rendern lässt. Das Ändern der key-Prop setzt ihn ebenfalls zurück.

react
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };
  static getDerivedStateFromError(error) { return { hasError: true, error }; }
  reset = () => this.setState({ hasError: false, error: null });
  render() {
    if (this.state.hasError) return <div><p>Failed.</p><button onClick={this.reset}>Try again</button></div>;
    return this.props.children;
  }
}

react-error-boundary-Bibliothek

Die react-error-boundary-Bibliothek bietet eine polierte, hook-freundliche Error Boundary ohne eine Klasse zu schreiben. FallbackComponent empfängt den Fehler und eine resetErrorBoundary-Funktion. resetKeys setzt automatisch zurück, wenn sich diese Werte ändern.

react
import { ErrorBoundary } from 'react-error-boundary';

<ErrorBoundary
  FallbackComponent={ErrorFallback}
  onError={(error, info) => logError(error, info)}
  onReset={() => window.location.reload()}
  resetKeys={[location.pathname]}
>
  <Routes />
</ErrorBoundary>

Async-Fehlerbehandlung

Error Boundaries fangen keine Fehler in Promises, setTimeout oder Event-Handlern ab. Um asynchrone Fehler einer Boundary zu melden, speichere den Fehler im State und werfe ihn während des Renders erneut. Die Boundary fängt ihn dann ab.

react
function AsyncComponent() {
  const [state, setState] = useState({ data: null, error: null });
  useEffect(() => {
    let active = true;
    fetchData()
      .then((data) => { if (active) setState({ data, error: null }); })
      .catch((error) => { if (active) setState({ data: null, error }); });
    return () => { active = false; };
  }, []);
  if (state.error) throw state.error;
  if (!state.data) return <Loading />;
  return <div>{state.data}</div>;
}
24

Performance Optimization

Virtualisierung

Virtualisierung rendert nur die sichtbaren Zeilen einer langen Liste und reduziert DOM-Knoten drastisch. Essenziell für Listen über 1000 Items — ohne sie erstickt der Browser an zehntausenden Knoten.

react
import { useVirtualizer } from '@tanstack/react-virtual';

function BigList({ items }) {
  const parentRef = useRef(null);
  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
  });
  return (
    <div ref={parentRef} style={{ height: 600, overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
        {virtualizer.getVirtualItems().map((vi) => (
          <div key={vi.key} style={{ position: 'absolute', top: vi.start, height: vi.size }}>
            {items[vi.index].name}
          </div>
        ))}
      </div>
    </div>
  );
}

Code Splitting

Code Splitting bricht das Bundle in Chunks, die on-demand geladen werden. Die wirkungsvollsten Splits sind Route-Level (jede Seite ist ein separater Chunk) und schwere Widgets (Chart-Bibliotheken, Editoren). Messe mit Bundle-Analyzern.

react
import { lazy, Suspense } from 'react';
const Admin = lazy(() => import('./Admin'));
const Chart = lazy(() => import('./Chart'));

function Page({ showChart }) {
  return (
    <Suspense fallback={<Skeleton />}>
      {showChart && <Chart data={data} />}
    </Suspense>
  );
}

Profiling mit DevTools

Der React DevTools Profiler zeichnet Render-Zeiten auf und zeigt, welche Komponenten neu gerendert wurden und warum. Suche nach "verschwendeten" Renders, bei denen sich Props nicht tatsächlich geändert haben — diese sind Kandidaten für React.memo.

react
// Use React DevTools Profiler to record renders
// Look for:
// - Components rendering too often
// - Long commit phases
// - Wasted renders (props didn't change)

// Wrap expensive renders to find bottlenecks
function MyComponent({ data }) {
  console.time('render');
  const result = heavyCompute(data);
  console.timeEnd('render');
  return <div>{result}</div>;
}

useDeferredValue

useDeferredValue verzögert das Aktualisieren eines Werts und lässt dringende Updates (Tippen) zuerst erfolgen. Das teure Render verwendet den verzögerten Wert, sodass es den Input nicht blockiert. isStale lässt dich einen dezenten visuellen Hinweis zeigen.

react
function Search({ query }) {
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;
  const results = useMemo(() => expensiveSearch(deferredQuery), [deferredQuery]);
  return <div style={{ opacity: isStale ? 0.7 : 1 }}>{results.map((r) => <div key={r.id}>{r.name}</div>)}</div>;
}

Concurrent Features

React 18 Concurrent Features halten die UI während schwerer Arbeit responsiv. useTransition und useDeferredValue lassen React Renders unterbrechen, um dringenden Input zu behandeln. Automatic Batching gruppiert mehrere setState-Aufrufe in ein Re-Render.

react
import { useTransition, useDeferredValue } from 'react';

// useTransition: mark updates as non-urgent
const [isPending, startTransition] = useTransition();
const filterResults = (q) => startTransition(() => setResults(search(q)));

// Automatic batching (React 18): state updates in promises batch automatically
fetch('/api').then(() => {
  setLoading(false);   //  \
  setData(data);       //   > single re-render
  setError(null);      //  /
});
25

Testing (React Testing Library)

Basis-Render & Query

render() mountet eine Komponente in einem Fake-DOM; screen queryt es. Bevorzuge Queries nach Role (getByRole) — sie spiegeln wider, wie assistive Tech die Seite sieht, und erzwingen Accessibility. userEvent simuliert echte Nutzer-Interaktionen.

react
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('increments on click', async () => {
  const user = userEvent.setup();
  render(<Counter />);
  expect(screen.getByText('Count: 0')).toBeInTheDocument();
  const button = screen.getByRole('button', { name: /increment/i });
  await user.click(button);
  expect(screen.getByText('Count: 1')).toBeInTheDocument();
});

Query-Varianten

getBy behauptet, dass das Element existiert (wirft sonst). queryBy ist zum Behaupten von Abwesenheit (gibt null zurück). findBy wartet auf asynchrone Elemente, bis sie erscheinen. Die All-Varianten behandeln mehrere Treffer.

react
// getBy: throws if 0 or >1 matches (strict)
screen.getByRole('button', { name: 'Submit' });

// queryBy: returns null if 0 matches (for assertions of absence)
expect(screen.queryByText('Error')).not.toBeInTheDocument();

// findBy: returns a Promise, waits for match (async)
const element = await screen.findByText('Loaded');

// getAllBy: returns array (multiple matches)
const items = screen.getAllByRole('listitem');

Events auslösen

userEvent (nicht das niedrigere fireEvent) ist die empfohlene Methode, um Interaktionen zu simulieren — es löst alle Events aus, die ein echter Nutzer auslösen würde (Focus, Input, Keydown, Click) in der richtigen Reihenfolge. Verwende immer await mit userEvent-Methoden.

react
import userEvent from '@testing-library/user-event';

test('form submission', async () => {
  const user = userEvent.setup();
  const onSubmit = vi.fn();
  render(<Form onSubmit={onSubmit} />);
  await user.type(screen.getByLabelText(/email/i), '[email protected]');
  await user.click(screen.getByRole('button', { name: /submit/i }));
  expect(onSubmit).toHaveBeenCalled();
});

waitFor & Async

waitFor pollt, bis die Assertion erfüllt ist oder ein Timeout auftritt. findBy* kombiniert waitFor und getBy für den häufigen Fall von "warte, bis dies erscheint." within schränkt Queries auf ein spezifisches Element ein.

react
import { waitFor, within } from '@testing-library/react';

test('shows data after fetch', async () => {
  render(<UserList />);
  await waitFor(() => {
    const list = screen.getByRole('list');
    expect(within(list).getAllByRole('listitem')).toHaveLength(3);
  });
});

// findBy is often cleaner than waitFor + getBy
test('shows data (cleaner)', async () => {
  render(<UserList />);
  expect(await screen.findAllByRole('listitem')).toHaveLength(3);
});

Mocking & Setup

MSW (Mock Service Worker) fängt Netzwerk-Requests auf Service-Worker-Ebene ab, sodass dein fetch-Code unverändert läuft. Richte Handler pro Test ein, setze sie zwischen Tests zurück und schließe nach allen.

react
import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  rest.get('/api/user', (req, res, ctx) => res(ctx.json({ name: 'Alice' })))
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test('shows user name', async () => {
  render(<UserProfile />);
  expect(await screen.findByText('Alice')).toBeInTheDocument();
});

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.