React Patterns
Idiomatische React 18/19-Muster für den Aufbau robuster, barrierefreier und performanter Komponentenbäume.
Wann aktivieren
- Schreiben oder Ändern von React-Funktionskomponenten, benutzerdefinierten Hooks oder Komponentenbäumen
- Überprüfen von JSX/TSX-Dateien
- Entwerfen von State-Shape oder Komponentenkomposition
- Migrieren von Klassenkomponenten oder älterem
forwardRef/useEffect-lastigem Code - Auswahl 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
- Anbinden von Datenabruf 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 Render-Zyklus hinzu, kann desynchronisieren und verschleiert den Datenfluss.
2. Side Effects außerhalb des Renders
Effekte, Mutationen, Netzwerkaufrufe und Subscriptions leben in Event-Handlern oder useEffect – niemals im Render-Body.
3. Komposition vor Vererbung
React hat kein Vererbungsmodell für Komponenten. Komponieren Sie mit children, Render Props oder Komponenten-Props.
Hooks-Disziplin
Siehe rules/react/hooks.md für das vollständige Regelset. Highlights:
- Nur auf oberster Ebene, niemals konditional
- Jede Subscription, jedes Intervall, jeden Listener aufräumen
- Funktionale 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 es als wichtig erweist - Einen benutzerdefinierten Hook nur extrahieren, wenn dieselbe Hook-Sequenz in 2+ Komponenten auftritt
Entscheidungsbaum für State-Position
Wird von einer Komponente verwendet?
-> useState darin
Wird von einer Eltern- und einigen Nachkommen verwendet?
-> auf den nächsten gemeinsamen Vorfahren heben
Wird von entfernten Zweigen verwendet UND hat niedrige Lesehäufigkeit (Theme, Auth, Locale)?
-> React Context
High-Frequency-Updates, die über den Baum geteilt werden?
-> externer Store (Zustand, Jotai, Redux Toolkit)
Wird von einem Server abgeleitet?
-> Server-State-Bibliothek (TanStack Query, SWR, RSC fetch)
Die meisten Seiten benötigen weder Context noch einen globalen Store. Widerstehen Sie der Abstraktion, bis das duplizierte Heben schmerzhaft wird.
Server / Client Components (RSC)
// Server Component - Standard, asynchron, versendet niemals JS für sich selbst
export default 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 über
<form action={...}>oder imperativ aus Event-Handlern aufrufen - Niemals eine Server Component aus einer Client Component-Datei
importieren– stattdessen viachildrenkomponieren
Suspense + Error Boundaries
<ErrorBoundary fallback={<ErrorView />}>
<Suspense fallback={<UserSkeleton />}>
<UserDetail id={id} />
</Suspense>
</ErrorBoundary>
- Suspense-Grenzen nahe an den Daten platzieren, nicht auf Router-Ebene – Inhalt progressiv anzeigen
- Error Boundary bleibt eine Klassen-API;
react-error-boundaryfür einen hook-freundlichen Wrapper verwenden - Eine Boundary fängt Fehler, die während des Renders, des Lifecycles und der Konstruktoren ihrer Kinder geworfen werden – NICHT in Event-Handlern oder asynchronem Code
Formulare
React 19-Formular-Aktionen (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-Elemente steuert, bei jedem Tastendruck formatiert oder eine Echtzeit-Validierung implementiert.
Komplexe Formulare
Für mehrstufige Formulare, dynamische Feld-Arrays oder validierung über Felder hinweg: Verwenden Sie eine Bibliothek (React Hook Form, TanStack Form). Eigenes State-Management für Formulare mit mehr als trivialer Komplexität ist eine Wartungsfalle.
Entscheidungsmatrix für Datenabruf
| Bedarf | Werkzeug |
|---|---|
| Pro-Request-Daten in Next.js App Router | RSC await fetch() |
| Client-seitiger Cache + Mutationen + Invalidierung | TanStack Query |
| Leichter Client-Cache + Revalidierung | SWR |
| Echtzeit-Subscriptions | Server-Sent Events, WebSockets oder die Subscription-API der Bibliothek |
| Einmalig "Feuer und Vergiss" | fetch() in einem Event-Handler |
Vermeiden Sie useEffect + fetch für Anwendungsdaten – Race Conditions, kein Cache, kein Retry, keine Suspense-Integration.
Kompositionsrezepte
Slot via children
<Layout>
<Header />
<Main>{content}</Main>
</Layout>
Benannte Slots
<Page header={<Nav />} sidebar={<Filters />}>
<Results />
</Page>
Compound Components (geteilter 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 / Function-as-Child
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 die gleiche Form zurückgibt – normalerweise sauberer.
Performance
Wann React.memo wirklich hilft
Wickeln Sie eine Komponente nur dann in React.memo, wenn:
- Sie häufig neu rendert
- Ihre Props zwischen den Rendern meist gleich sind
- Ihr Render messbar teuer ist
React.memo fügt bei jedem Render einen Gleichheitscheck hinzu. Wenn sich Props bei den meisten Rendern unterscheiden, ist der Check reiner Overhead.
Renderkaskaden vermeiden
- State eher nach unten als nach oben heben
- Context aufteilen: ein Context pro Belang, sodass eine Änderung von
themeContextkeine Auth-Konsumenten neu rendert useSyncExternalStorefür externe State-Bibliotheken verwenden – erforderlich für sicheres Concurrent Rendering
Listen
- Stabile
key-Props bereitstellen (Datenbank-ID, nicht Array-Index) - Lange Listen mit
@tanstack/react-virtualoderreact-windowvirtualisieren, sobald die sichtbare Anzahl ~50 mit nicht-trivialen Zeilen überschreitet
Barrierefreiheit zuerst
- Immer semantisches HTML rendern (
<button>,<a>,<nav>,<main>), bevorrole-Attribute verwendet werden - Jedes interaktive Element muss per Tastatur erreichbar sein
- Formulareingaben benötigen Labels –
<label htmlFor>oderaria-label, wenn visuell durch ein Icon beschriftet - Fokus bei Routenwechseln und Modal-Öffnen/-Schließen verwalten
axein Komponententests ausführen (siehe skills/react-testing)- Querverweis: skills/accessibility/SKILL.md behandelt WCAG-Kriterien und Pattern-Bibliotheken
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 jeweiligen Routers – das sind Framework-Belange, die auf dem React-Kern aufbauen.
Nicht abgedeckt (Verweissektionen)
- Next.js-Spezifika: App Router-Datenladen, 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, aber der Remix-Dokumentation folgen
Verwandtes
- Regeln: rules/react/ – Codestyle, Hooks, Muster, Sicherheit, Tests
- Skills: react-performance für das von Vercel abgeleitete Performance-Regelset, frontend-patterns für framework-übergreifende UI-Belange, accessibility, angular-developer für Framework-Vergleich
- Agents:
react-reviewerfür Code-Review,react-build-resolverfür Build/Bundler-Fehler - Befehle:
/react-review,/react-build,/react-test
Beispiele
Benutzerdefinierter Hook für debounce-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 Renderkaskaden 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 Benachrichtigungen ändern