React Patterns
Idiomatische React 18/19 Muster für den Bau robuster, zugänglicher und leistungsfähiger Komponentenbäume.
Wann aktivieren
- Schreiben oder Ändern von React-Funktionskomponenten, benutzerdefinierten Hooks oder Komponentenbäumen
- Überprüfen von JSX/TSX-Dateien
- Entwerfen des State-Shape oder der Komponentenkomposition
- Migrieren von Klassenkomponenten oder altem
forwardRef/useEffect-lastigem Code - Wählen zwischen lokalem State, gehobenem State, Context und externen Stores
- Arbeiten mit Server Components / Client Components (Next.js App Router, RSC)
- Implementieren von Formularen mit React 19 Aktionen oder kontrollierten Eingaben
- Verdrahten des Datenabrufs mit TanStack Query / SWR / RSC
Kernprinzipien
1. Render ist eine reine Funktion von Props und State
// Gut: während des Renders ableiten
function Cart({ items }: { items: CartItem[] }) {
const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
return <span>{formatMoney(total)}</span>;
}
// Schlecht: abgeleiteter State separat gespeichert
function Cart({ items }: { items: CartItem[] }) {
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(items.reduce((sum, i) => sum + i.price * i.qty, 0));
}, [items]);
return <span>{formatMoney(total)}</span>;
}
Abgeleiteter State in useEffect fügt einen Renderzyklus hinzu, kann desynchronisieren und verschleiert den Datenfluss.
2. Seiteneffekte außerhalb des Renders
Effekte, Mutationen, Netzwerkaufrufe und Subskriptionen leben in Ereignisbehandlern oder useEffect – niemals im Render-Body.
3. Komposition über Vererbung
React hat kein Vererbungsmodell für Komponenten. Komponiere mit children, Render-Props oder Komponenten-Props.
Hooks-Disziplin
Siehe rules/react/hooks.md für den vollständigen Regelsatz. Highlights:
- Nur auf oberster Ebene, niemals bedingt
- Jede Subskription, jeden Timer, jeden Listener aufräumen
- Funktionaler Updater (
setX(prev => prev + 1)) wenn neuer State vom alten abhängt - Standardposition: nicht memoizen –
useMemo/useCallbacknur hinzufügen, wenn ein Profiler oder eine Abhängigkeitskette zeigt, dass es wichtig ist - Einen benutzerdefinierten Hook nur extrahieren, wenn die gleiche Hook-Sequenz in 2+ Komponenten vorkommt
Entscheidungsbaum für die State-Positionierung
Wird von einer Komponente verwendet?
-> useState darin
Wird von der Elternkomponente + einigen Nachkommen verwendet?
-> zum nächsten gemeinsamen Vorfahren heben
Wird über entfernte Äste verwendet UND selten gelesen (Theme, Auth, Locale)?
-> React Context
Häufige Updates, die über den gesamten Baum geteilt werden?
-> externer Store (Zustand, Jotai, Redux Toolkit)
Abgeleitet von einem Server?
-> Server-State-Bibliothek (TanStack Query, SWR, RSC fetch)
Die meisten Seiten benötigen weder Context noch einen globalen Store. Widerstehe der Abstraktion, bis das duplizierte Heben schmerzhaft wird.
Server / Client Components (RSC)
// Server Component - Standard, async, sendet nie JS für sich selbst
async function ProductPage({ params }: { params: { id: string } }) {
const product = await db.product.findUnique({ where: { id: params.id } });
if (!product) notFound();
return <ProductView product={product} />;
}
// Client Component - mit "use client" aktivieren
"use client";
export function AddToCartButton({ productId }: { productId: string }) {
const [pending, startTransition] = useTransition();
return (
<button
disabled={pending}
onClick={() => startTransition(() => addToCart(productId))}
>
{pending ? "Adding..." : "Add to cart"}
</button>
);
}
Grenzen:
- Server -> Client: serialisierbare Props oder
childrenübergeben - Client -> Server: Server Actions via
<form action={...}>oder imperativ aus Ereignisbehandlern aufrufen - Niemals eine Server Component aus einer Client Component Datei
importieren– stattdessen viachildrenkomponieren
Suspense + Fehlergrenzen
<ErrorBoundary fallback={<ErrorView />}>
<Suspense fallback={<UserSkeleton />}>
<UserDetail id={id} />
</Suspense>
</ErrorBoundary>
- Platzieren Sie Suspense-Grenzen nahe an den Daten, nicht am Routen-Stamm – zeigen Sie Inhalte progressiv an
- Error Boundary bleibt eine Klassen-API; verwenden Sie
react-error-boundaryfür einen hook-freundlichen Wrapper - Eine Grenze fängt Fehler, die während des Renders, des Lebenszyklus und der Konstruktoren ihrer Kinder geworfen werden – NICHT in Ereignisbehandlern oder asynchronem Code
Formulare
React 19 Formularaktionen (bevorzugt für neuen Code)
"use client";
import { useActionState } from "react";
const initial = { error: null as string | null };
async function updateUserAction(_prev: typeof initial, formData: FormData) {
"use server";
const parsed = UserSchema.safeParse(Object.fromEntries(formData));
if (!parsed.success) return { error: "Invalid input" };
await db.user.update({ where: { id: parsed.data.id }, data: parsed.data });
return { error: null };
}
export function UserForm() {
const [state, formAction, pending] = useActionState(updateUserAction, initial);
return (
<form action={formAction}>
<input name="name" required />
<button type="submit" disabled={pending}>Save</button>
{state.error && <p role="alert">{state.error}</p>}
</form>
);
}
Kontrollierte Eingaben
Verwenden Sie kontrollierte Eingaben, wenn der Wert andere UI steuert, bei jedem Tastendruck formatiert oder Echtzeit-Validierung implementiert.
Komplexe Formulare
Für mehrstufige Formulare, dynamische Feldarrays oder feldübergreifende Validierung: Verwenden Sie eine Bibliothek (React Hook Form, TanStack Form). Eigenes State-Management für Formulare über trivialer Komplexität hinaus ist eine Wartungsfalle.
Datenabruf-Entscheidungsmatrix
| Bedarf | Tool |
|---|---|
| Daten pro Anfrage im Next.js App Router | RSC await fetch() |
| Client-seitiger Cache + Mutationen + Invalidierung | TanStack Query |
| Leichter Client-Cache + Neuvalidierung | SWR |
| Echtzeit-Subskriptionen | Server-Sent Events, WebSockets oder die Subskriptions-API der Bibliothek |
| Einmaliges Feuer-und-Vergiss | fetch() in einem Ereignisbehandler |
Vermeiden Sie useEffect + fetch für Anwendungsdaten – Wettlaufsituationen, kein Cache, kein Wiederholungsversuch, keine Suspense-Integration.
Kompositionsrezepte
Slot via children
<Layout>
<Header />
<Main>{content}</Main>
</Layout>
Benannte Slots
<Page header={<Nav />} sidebar={<Filters />}>
<Results />
</Page>
Verbundkomponenten (gemeinsamer State via Context)
<Tabs defaultValue="profile">
<Tabs.List>
<Tabs.Trigger value="profile">Profile</Tabs.Trigger>
<Tabs.Trigger value="settings">Settings</Tabs.Trigger>
</Tabs.List>
<Tabs.Panel value="profile"><Profile /></Tabs.Panel>
<Tabs.Panel value="settings"><Settings /></Tabs.Panel>
</Tabs>
Render-Prop / Funktion-als-Kind
Nützlich, wenn die Elternkomponente Parameter an die gerenderte Ausgabe übergeben muss:
<DataLoader id={id}>
{({ data, isLoading }) => isLoading ? <Spinner /> : <UserCard user={data} />}
</DataLoader>
Moderne Alternative: ein Hook (useData(id)), der dieselbe Form zurückgibt – normalerweise sauberer.
Leistung
Wann React.memo wirklich hilft
Wickeln Sie eine Komponente nur in React.memo, wenn:
- Sie häufig neu rendert
- Ihre Props zwischen den Renders normalerweise gleich sind
- Ihr Render messbar teuer ist
React.memo fügt bei jedem Render eine Gleichheitsprüfung hinzu. Wenn sich die Props bei den meisten Renders unterscheiden, ist die Prüfung reiner Overhead.
Render-Kaskaden vermeiden
- Heben Sie State eher nach unten als nach oben, wo möglich
- Teilen Sie Context: einen Context pro Belang, sodass eine Änderung an
themeContextnicht Auth-Konsumenten neu rendert - Verwenden Sie
useSyncExternalStorefür externe State-Bibliotheken – erforderlich für sicheres gleichzeitiges Rendern
Listen
- Geben Sie stabile
key-Props (Datenbank-ID, nicht Array-Index) - Virtualisieren Sie lange Listen mit
@tanstack/react-virtualoderreact-window, sobald die sichtbare Elementanzahl ~50 mit nicht-trivialen Zeilen überschreitet
Barrierefreiheit zuerst komponieren
- Rendern Sie immer semantisches HTML (
<button>,<a>,<nav>,<main>), bevor Sie zurole-Attributen greifen - Jedes interaktive Element muss per Tastatur erreichbar sein
- Formulareingaben benötigen Labels –
<label htmlFor>oderaria-label, wenn sie visuell durch ein Icon beschriftet sind - Verwalten Sie den Fokus bei Routenwechseln und Modal-Öffnen/Schließen
- Führen Sie
axein Komponententests aus (siehe skills/react-testing) - Querverweis: skills/accessibility/SKILL.md behandelt WCAG-Kriterien und Musterbibliotheken
Routing
Diese Skill ist Router-unabhängig. Die obigen Muster funktionieren mit React Router, TanStack Router, Next.js App Router, Remix Router. Router-spezifische Muster (Loader, Actions, verschachtelte Layouts) folgen der Dokumentation des Routers – das sind Framework-Belange, die auf dem React-Kern aufbauen.
Außerhalb des Rahmens (Verweisbereiche)
- Next.js-Spezifika: App Router Daten laden, Route Handler, Middleware, Parallel Routes – separates Thema, Next.js-Dokumentation verwenden
- React Native: Plattformspezifische Muster unterscheiden sich genug, um eine separate
react-native-patterns-Skill zu rechtfertigen (noch nicht vorhanden) - Remix: Loader/Action-Konventionen überschneiden sich mit RSC, folgen aber der Remix-Dokumentation
Verwandt
- Regeln: rules/react/ – Codestil, Hooks, Muster, Sicherheit, Tests
- Skills: react-performance für den von Vercel abgeleiteten Leistungsregelsatz, frontend-patterns für frameworkübergreifende UI-Belange, accessibility, angular-developer für Framework-Vergleich
- Agenten:
react-reviewerfür Code-Review,react-build-resolverfür Build-/Bundler-Fehler - Befehle:
/react-review,/react-build,/react-test
Beispiele
Benutzerdefinierter Hook für verzögerte Suche
function useDebounce<T>(value: T, delay = 300): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}
function SearchBox() {
const [query, setQuery] = useState("");
const debounced = useDebounce(query, 300);
const { data } = useQuery({
queryKey: ["search", debounced],
queryFn: () => searchApi(debounced),
enabled: debounced.length > 0,
});
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<Results items={data ?? []} />
</>
);
}
Optimistische UI mit React 19 useOptimistic
"use client";
import { useOptimistic } from "react";
export function MessageList({ messages }: { messages: Message[] }) {
const [optimistic, addOptimistic] = useOptimistic(
messages,
(state, newMessage: Message) => [...state, newMessage],
);
async function send(formData: FormData) {
const text = String(formData.get("text"));
addOptimistic({ id: "pending", text, sender: "me" });
await saveMessage(text);
}
return (
<>
<ul>{optimistic.map((m) => <li key={m.id}>{m.text}</li>)}</ul>
<form action={send}>
<input name="text" />
<button type="submit">Send</button>
</form>
</>
);
}
Context aufteilen, um Render-Kaskaden zu vermeiden
// Zwei Contexts: einer ändert sich selten, einer häufig
const ThemeContext = createContext<Theme>("light");
const NotificationsContext = createContext<Notification[]>([]);
// Eine Komponente, die nur ThemeContext konsumiert, rendert NICHT neu, wenn sich Notifications ändern