Component Patterns
Composition, compound components, custom hooks, headless UI, and reusable patterns for React — structured for AI-assisted development.
Component Patterns
Composition, compound components, custom hooks, headless UI, and reusable patterns for React — structured for AI-assisted development.
Principles
1. Composition Over Configuration
When a component grows past 10-15 props, it's a sign you're configuring when you should be composing. Instead of a monolithic component with props for every variation, compose small, focused components:
Configuration (bad at scale):
<Card
title="Order Summary"
subtitle="3 items"
icon={<ShoppingCartIcon />}
headerAction={<Button>Edit</Button>}
footer={<Total amount={99.99} />}
bordered
padded
/>Composition (scales well):
<Card>
<Card.Header>
<ShoppingCartIcon />
<div>
<Card.Title>Order Summary</Card.Title>
<Card.Subtitle>3 items</Card.Subtitle>
</div>
<Button>Edit</Button>
</Card.Header>
<Card.Body>{/* content */}</Card.Body>
<Card.Footer>
<Total amount={99.99} />
</Card.Footer>
</Card>Composition is more flexible, more readable, and the AI can generate more creative layouts without needing to know 30 prop combinations.
2. Compound Components
Compound components share implicit state through Context, letting users compose sub-components in any order:
// Usage — the developer controls layout, Tabs handles state
<Tabs defaultValue="overview">
<Tabs.List>
<Tabs.Trigger value="overview">Overview</Tabs.Trigger>
<Tabs.Trigger value="analytics">Analytics</Tabs.Trigger>
<Tabs.Trigger value="settings">Settings</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="overview">...</Tabs.Content>
<Tabs.Content value="analytics">...</Tabs.Content>
<Tabs.Content value="settings">...</Tabs.Content>
</Tabs>The parent (Tabs) provides context. Children (Tabs.Trigger, Tabs.Content) consume it. The developer arranges them freely — Tabs doesn't care about the DOM structure between its children.
3. Custom Hooks
Custom hooks extract reusable stateful logic from components. They're the primary abstraction in React:
Rules for custom hooks:
- Name starts with
use - Encapsulates one concern (not a kitchen sink)
- Returns only what consumers need
- Handles cleanup (unsubscribe, abort, clear)
- Is testable in isolation
Good custom hook candidates:
- Data fetching logic that's reused across components
- Browser API integrations (media queries, intersection observer, clipboard)
- Complex state transitions (multi-step wizard, undo/redo)
- Debounced/throttled values
4. Headless / Unstyled Libraries
Headless UI libraries provide behavior and accessibility without styling:
- Radix UI — the most popular, extensive primitive collection
- React Aria (Adobe) — the most accessible, used by Spectrum
- Headless UI (Tailwind Labs) — simpler API, fewer components
Why headless: You get keyboard navigation, focus management, ARIA attributes, and screen reader support for free. Then you add your own styles with Tailwind. Don't rebuild a combobox from scratch — use Radix or React Aria and style it.
5. Render Props
Render props pass a function as children (or a prop) that receives data and returns JSX:
<Combobox items={products}>
{({ filteredItems, query, setQuery }) => (
<>
<input value={query} onChange={e => setQuery(e.target.value)} />
<ul>
{filteredItems.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
</>
)}
</Combobox>When to use render props over hooks: When the component provides both state AND DOM elements (like portals, popovers), or when you need to expose multiple render slots. In most cases, prefer custom hooks.
6. Higher-Order Components (HOCs)
HOCs wrap a component to add behavior. They're less common since hooks, but still useful for cross-cutting concerns:
function withAuth<T extends object>(Component: React.ComponentType<T>) {
return function AuthenticatedComponent(props: T) {
const { user, isLoading } = useAuth();
if (isLoading) return <Skeleton />;
if (!user) redirect("/login");
return <Component {...props} />;
};
}
const ProtectedDashboard = withAuth(Dashboard);When to use HOCs: Rare in modern React. Most use cases are better served by hooks + composition. Consider HOCs for wrapping entire routes with auth/layout logic.
7. Children Patterns
children as ReactNode — the most common pattern:
function Card({ children }: { children: React.ReactNode }) {
return <div className="rounded-lg border p-4">{children}</div>;
}Render function children — when children need data from the parent:
<DataFetcher url="/api/data">
{(data) => <Display data={data} />}
</DataFetcher>Slots pattern — named children via props:
interface DialogProps {
trigger: React.ReactNode;
title: React.ReactNode;
children: React.ReactNode;
footer?: React.ReactNode;
}Avoid React.Children and cloneElement — they're fragile and break with wrappers. Use Context or explicit slots instead.
8. Ref Forwarding
Any reusable component that wraps a DOM element should forward refs:
import { forwardRef } from "react";
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
error?: string;
}
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, error, className, ...props }, ref) => {
const id = useId();
return (
<div>
<label htmlFor={id} className="block text-sm font-medium">
{label}
</label>
<input
ref={ref}
id={id}
className={cn(
"mt-1 w-full rounded border px-3 py-2",
error && "border-red-500",
className,
)}
aria-describedby={error ? `${id}-error` : undefined}
aria-invalid={!!error}
{...props}
/>
{error && (
<p id={`${id}-error`} className="mt-1 text-sm text-red-600">
{error}
</p>
)}
</div>
);
},
);
Input.displayName = "Input";9. Provider Pattern
When multiple providers nest deeply, compose them:
// Before — deeply nested providers
function App() {
return (
<ThemeProvider>
<AuthProvider>
<QueryClientProvider client={queryClient}>
<ToastProvider>
<ModalProvider>
{children}
</ModalProvider>
</ToastProvider>
</QueryClientProvider>
</AuthProvider>
</ThemeProvider>
);
}
// After — composed providers
function composeProviders(...providers: React.FC<{ children: React.ReactNode }>[]) {
return function ComposedProviders({ children }: { children: React.ReactNode }) {
return providers.reduceRight(
(child, Provider) => <Provider>{child}</Provider>,
children,
);
};
}
const Providers = composeProviders(
ThemeProvider,
AuthProvider,
({ children }) => <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>,
ToastProvider,
ModalProvider,
);
function App() {
return <Providers>{children}</Providers>;
}10. Container vs Presentational in the RSC Era
The old "container vs presentational" split maps naturally to Server vs Client Components in Next.js:
Server Components (containers):
- Fetch data
- Access server resources (database, file system, env vars)
- Pass data down to presentational components
- Zero client-side JavaScript
Client Components (presentational + interactive):
- Receive data via props
- Handle user interaction (clicks, forms, drag-and-drop)
- Manage local UI state
- Use browser APIs
// Server Component — fetches data, passes it down
export default async function UserProfilePage({ params }: Props) {
const { id } = await params;
const user = await getUser(id);
const posts = await getUserPosts(id);
return (
<div>
<UserHeader user={user} /> {/* Server — static display */}
<FollowButton userId={id} /> {/* Client — interactive */}
<PostList posts={posts} /> {/* Server — static display */}
</div>
);
}LLM Instructions
Building Reusable Components
When generating reusable components:
- Accept
className?: stringand merge withcn() - Forward refs with
forwardReffor any component wrapping a DOM element - Use compound components for complex UI (Tabs, Accordion, Select, Menu)
- Accept
children: React.ReactNodefor content projection - Keep the API surface small — prefer composition over props
- Export named:
export function Button()notexport default
Extracting Custom Hooks
When creating custom hooks:
- Name with
useprefix:useLocalStorage,useMediaQuery,useDebounce - Accept configuration as parameters, return only what consumers need
- Handle cleanup: return cleanup in useEffect, abort controllers for fetch
- Type return values explicitly for documentation
- Consider returning tuple
[value, setter]for simple hooks or object{ value, isLoading, error }for complex ones
Using Headless Libraries
When implementing UI patterns, prefer headless libraries over custom implementations:
- Dialogs/Modals → Radix Dialog or React Aria Dialog
- Dropdowns/Menus → Radix DropdownMenu
- Comboboxes/Autocomplete → Radix Combobox or React Aria ComboBox
- Tooltips → Radix Tooltip
- Tabs → Radix Tabs
- Accordions → Radix Accordion
Wrap them with your project's Tailwind styles and export them as your design system components.
Composition with Children and Slots
When a component needs flexible content areas:
- Use
childrenfor the primary content slot - Use named props for additional slots:
header,footer,trigger,icon - For complex layouts, use compound components with Context
- Avoid
React.Children.mapandcloneElement— they break with wrappers
Deciding Between Patterns
- Simple UI → Just a component with props
- Styled variants → cva (class-variance-authority)
- Complex with flexible layout → Compound components
- Reusable logic → Custom hook
- Accessible primitive → Headless library + Tailwind wrapper
- Cross-cutting concern → HOC or hook (prefer hook)
- Data → JSX mapping with flexibility → Render prop or generic component
Examples
1. Compound Tabs Component
A fully accessible tabs implementation using compound components:
"use client";
import {
createContext,
useContext,
useState,
useId,
type ReactNode,
} from "react";
import { cn } from "@/lib/utils";
// Context
interface TabsContextValue {
activeTab: string;
setActiveTab: (value: string) => void;
baseId: string;
}
const TabsContext = createContext<TabsContextValue | null>(null);
function useTabsContext() {
const context = useContext(TabsContext);
if (!context) throw new Error("Tabs compound components must be used within <Tabs>");
return context;
}
// Root
interface TabsProps {
defaultValue: string;
children: ReactNode;
className?: string;
onChange?: (value: string) => void;
}
export function Tabs({ defaultValue, children, className, onChange }: TabsProps) {
const [activeTab, setActiveTab] = useState(defaultValue);
const baseId = useId();
function handleChange(value: string) {
setActiveTab(value);
onChange?.(value);
}
return (
<TabsContext.Provider value={{ activeTab, setActiveTab: handleChange, baseId }}>
<div className={className}>{children}</div>
</TabsContext.Provider>
);
}
// List
Tabs.List = function TabsList({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return (
<div
role="tablist"
className={cn("flex gap-1 border-b", className)}
>
{children}
</div>
);
};
// Trigger
Tabs.Trigger = function TabsTrigger({
value,
children,
className,
}: {
value: string;
children: ReactNode;
className?: string;
}) {
const { activeTab, setActiveTab, baseId } = useTabsContext();
const isActive = activeTab === value;
return (
<button
role="tab"
id={`${baseId}-tab-${value}`}
aria-controls={`${baseId}-panel-${value}`}
aria-selected={isActive}
tabIndex={isActive ? 0 : -1}
onClick={() => setActiveTab(value)}
className={cn(
"px-4 py-2 text-sm font-medium transition-colors -mb-px",
isActive
? "border-b-2 border-brand-500 text-brand-600"
: "text-gray-500 hover:text-gray-700",
className,
)}
>
{children}
</button>
);
};
// Content
Tabs.Content = function TabsContent({
value,
children,
className,
}: {
value: string;
children: ReactNode;
className?: string;
}) {
const { activeTab, baseId } = useTabsContext();
if (activeTab !== value) return null;
return (
<div
role="tabpanel"
id={`${baseId}-panel-${value}`}
aria-labelledby={`${baseId}-tab-${value}`}
tabIndex={0}
className={cn("pt-4", className)}
>
{children}
</div>
);
};
// Usage
function SettingsPage() {
return (
<Tabs defaultValue="general">
<Tabs.List>
<Tabs.Trigger value="general">General</Tabs.Trigger>
<Tabs.Trigger value="security">Security</Tabs.Trigger>
<Tabs.Trigger value="notifications">Notifications</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="general">
<GeneralSettings />
</Tabs.Content>
<Tabs.Content value="security">
<SecuritySettings />
</Tabs.Content>
<Tabs.Content value="notifications">
<NotificationSettings />
</Tabs.Content>
</Tabs>
);
}2. Custom Hooks Collection
Four practical custom hooks:
// hooks/use-debounce.ts
import { useState, useEffect } from "react";
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Usage
function Search() {
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 300);
// Fetch with debouncedQuery instead of query
}// hooks/use-media-query.ts
import { useState, useEffect } from "react";
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false);
useEffect(() => {
const media = window.matchMedia(query);
setMatches(media.matches);
function handleChange(e: MediaQueryListEvent) {
setMatches(e.matches);
}
media.addEventListener("change", handleChange);
return () => media.removeEventListener("change", handleChange);
}, [query]);
return matches;
}
// Usage
function Layout() {
const isMobile = useMediaQuery("(max-width: 768px)");
return isMobile ? <MobileNav /> : <DesktopNav />;
}// hooks/use-click-outside.ts
import { useEffect, useRef, type RefObject } from "react";
export function useClickOutside<T extends HTMLElement>(
handler: () => void,
): RefObject<T | null> {
const ref = useRef<T>(null);
useEffect(() => {
function handleClick(event: MouseEvent) {
if (ref.current && !ref.current.contains(event.target as Node)) {
handler();
}
}
document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
}, [handler]);
return ref;
}
// Usage
function Dropdown() {
const [open, setOpen] = useState(false);
const ref = useClickOutside<HTMLDivElement>(() => setOpen(false));
return <div ref={ref}>{open && <DropdownMenu />}</div>;
}// hooks/use-copy-to-clipboard.ts
import { useState, useCallback } from "react";
export function useCopyToClipboard() {
const [copied, setCopied] = useState(false);
const copy = useCallback(async (text: string) => {
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
setCopied(false);
}
}, []);
return { copied, copy };
}
// Usage
function CodeBlock({ code }: { code: string }) {
const { copied, copy } = useCopyToClipboard();
return (
<div className="relative">
<pre>{code}</pre>
<button onClick={() => copy(code)}>
{copied ? "Copied!" : "Copy"}
</button>
</div>
);
}3. Radix Dialog Wrapper
Wrapping a headless Radix Dialog with project styles:
"use client";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
import { forwardRef, type ReactNode } from "react";
export function Dialog({ children, ...props }: DialogPrimitive.DialogProps) {
return <DialogPrimitive.Root {...props}>{children}</DialogPrimitive.Root>;
}
Dialog.Trigger = DialogPrimitive.Trigger;
Dialog.Content = forwardRef<
HTMLDivElement,
DialogPrimitive.DialogContentProps & { title: string; description?: string }
>(({ title, description, children, className, ...props }, ref) => (
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay className="fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-fade-in" />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2",
"rounded-xl bg-white p-6 shadow-xl dark:bg-gray-900",
"data-[state=open]:animate-slide-up",
className,
)}
{...props}
>
<div className="mb-4">
<DialogPrimitive.Title className="text-lg font-semibold">
{title}
</DialogPrimitive.Title>
{description && (
<DialogPrimitive.Description className="mt-1 text-sm text-gray-500">
{description}
</DialogPrimitive.Description>
)}
</div>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
));
Dialog.Content.displayName = "DialogContent";
// Usage
function DeleteConfirmation({ onConfirm }: { onConfirm: () => void }) {
return (
<Dialog>
<Dialog.Trigger asChild>
<button className="text-red-600">Delete</button>
</Dialog.Trigger>
<Dialog.Content
title="Delete Item"
description="This action cannot be undone."
>
<div className="flex justify-end gap-3">
<DialogPrimitive.Close asChild>
<button className="rounded px-4 py-2 text-gray-600 hover:bg-gray-100">
Cancel
</button>
</DialogPrimitive.Close>
<button
onClick={onConfirm}
className="rounded bg-red-600 px-4 py-2 text-white hover:bg-red-700"
>
Delete
</button>
</div>
</Dialog.Content>
</Dialog>
);
}4. Provider Composition Utility
Cleanly compose multiple providers without deep nesting:
// lib/compose-providers.tsx
import type { ComponentType, ReactNode } from "react";
type Provider = ComponentType<{ children: ReactNode }>;
export function composeProviders(...providers: Provider[]) {
return function ComposedProviders({ children }: { children: ReactNode }) {
return providers.reduceRight(
(child, Provider) => <Provider>{child}</Provider>,
children,
);
};
}// app/providers.tsx
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ThemeProvider } from "next-themes";
import { AuthProvider } from "@/contexts/auth";
import { ToastProvider } from "@/contexts/toast";
import { composeProviders } from "@/lib/compose-providers";
import { useState } from "react";
function QueryProvider({ children }: { children: React.ReactNode }) {
const [client] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: { staleTime: 5 * 60 * 1000 },
},
}),
);
return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
}
function Theme({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
);
}
export const Providers = composeProviders(
Theme,
QueryProvider,
AuthProvider,
ToastProvider,
);// app/layout.tsx
import { Providers } from "./providers";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationMismatch>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}5. Generic List with Render Props
A reusable list component that handles loading, empty, and error states:
interface ListProps<T> {
items: T[];
isLoading?: boolean;
error?: Error | null;
renderItem: (item: T, index: number) => ReactNode;
renderEmpty?: () => ReactNode;
renderError?: (error: Error) => ReactNode;
keyExtractor: (item: T) => string;
className?: string;
}
export function List<T>({
items,
isLoading,
error,
renderItem,
renderEmpty,
renderError,
keyExtractor,
className,
}: ListProps<T>) {
if (isLoading) {
return (
<div className="space-y-3">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="h-16 animate-pulse rounded-lg bg-gray-200" />
))}
</div>
);
}
if (error) {
return renderError ? (
renderError(error)
) : (
<div className="rounded-lg border border-red-200 bg-red-50 p-4 text-red-700">
{error.message}
</div>
);
}
if (items.length === 0) {
return renderEmpty ? (
renderEmpty()
) : (
<div className="py-12 text-center text-gray-500">No items found</div>
);
}
return (
<ul className={cn("space-y-2", className)}>
{items.map((item, index) => (
<li key={keyExtractor(item)}>{renderItem(item, index)}</li>
))}
</ul>
);
}
// Usage
function UserList() {
const { data: users = [], isLoading, error } = useUsers();
return (
<List
items={users}
isLoading={isLoading}
error={error}
keyExtractor={user => user.id}
renderItem={user => (
<div className="flex items-center gap-3 rounded-lg border p-3">
<Avatar src={user.avatar} alt={user.name} />
<div>
<p className="font-medium">{user.name}</p>
<p className="text-sm text-gray-500">{user.email}</p>
</div>
</div>
)}
renderEmpty={() => (
<div className="py-12 text-center">
<p className="text-gray-500">No users yet</p>
<button className="mt-2 text-brand-500">Invite users</button>
</div>
)}
/>
);
}Common Mistakes
1. Giant Components With 30+ Props
Wrong:
<DataTable
data={data}
columns={columns}
sortable
filterable
paginated
pageSize={10}
onSort={handleSort}
onFilter={handleFilter}
onPageChange={handlePage}
selectable
onSelect={handleSelect}
expandable
renderExpanded={row => <Details row={row} />}
headerActions={<Button>Export</Button>}
emptyMessage="No data"
loading={isLoading}
error={error}
// ... 15 more props
/>Fix: Use composition:
<DataTable data={data}>
<DataTable.Toolbar>
<DataTable.Search />
<DataTable.Filter column="status" options={statusOptions} />
<Button>Export</Button>
</DataTable.Toolbar>
<DataTable.Content columns={columns} />
<DataTable.Pagination pageSize={10} />
</DataTable>2. No Ref Forwarding on Reusable Components
Wrong:
function Input({ label, ...props }: InputProps) {
return <input {...props} />;
// Parent can't call inputRef.current.focus()
}Fix:
const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, ...props }, ref) => (
<div>
<label>{label}</label>
<input ref={ref} {...props} />
</div>
),
);3. Reimplementing Accessibility
Wrong:
function Dropdown() {
const [open, setOpen] = useState(false);
return (
<div>
<button onClick={() => setOpen(!open)}>Menu</button>
{open && <div className="absolute">{/* items */}</div>}
</div>
);
// Missing: keyboard nav, focus trap, Escape to close, ARIA attributes, click outside
}Fix: Use a headless library:
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
function Dropdown() {
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger>Menu</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item>Profile</DropdownMenu.Item>
<DropdownMenu.Item>Settings</DropdownMenu.Item>
<DropdownMenu.Item>Logout</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
// All a11y handled: keyboard, focus trap, ARIA, click outside
}4. Prop Drilling Over Composition
Wrong:
function Page({ user }) {
return <Layout user={user}><Content user={user} /></Layout>;
}
function Layout({ user, children }) {
return <div><Header user={user} />{children}</div>;
}
function Header({ user }) {
return <nav><Avatar user={user} /></nav>;
}Fix: Use composition (children) or context:
function Page({ user }) {
return (
<Layout header={<Header><Avatar user={user} /></Header>}>
<Content user={user} />
</Layout>
);
}
function Layout({ header, children }) {
return <div>{header}{children}</div>;
}5. Overusing cloneElement
Wrong:
function ButtonGroup({ children }) {
return (
<div className="flex gap-2">
{React.Children.map(children, child =>
React.cloneElement(child, { size: "sm", variant: "outline" }),
)}
</div>
);
// Breaks if children are wrapped in a fragment or conditional
}Fix: Use Context or explicit props:
const ButtonGroupContext = createContext<{ size: string; variant: string } | null>(null);
function ButtonGroup({ children }) {
return (
<ButtonGroupContext.Provider value={{ size: "sm", variant: "outline" }}>
<div className="flex gap-2">{children}</div>
</ButtonGroupContext.Provider>
);
}6. Custom Hooks That Do Too Much
Wrong:
function useEverything() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState("light");
const [notifications, setNotifications] = useState([]);
const [sidebarOpen, setSidebarOpen] = useState(true);
// 200 lines of mixed concerns
}Fix: One hook per concern:
function useAuth() { /* user state */ }
function useTheme() { /* theme state */ }
function useNotifications() { /* notification state */ }7. Untyped Data Components
Wrong:
function DataTable({ data, columns }) {
// data is any, columns is any — no autocompletion, no type checking
}Fix: Use generics:
interface Column<T> {
key: keyof T;
header: string;
render?: (value: T[keyof T], row: T) => ReactNode;
}
function DataTable<T>({ data, columns }: { data: T[]; columns: Column<T>[] }) {
// Fully typed — columns are constrained to keys of T
}8. Ten Nested Providers
Wrong:
<Provider1>
<Provider2>
<Provider3>
<Provider4>
<Provider5>
<Provider6>
<Provider7>
{children}
</Provider7>
</Provider6>
</Provider5>
</Provider4>
</Provider3>
</Provider2>
</Provider1>Fix: Use the composeProviders utility (see Example 4).
9. Structure-Dependent Children
Wrong:
function Tabs({ children }) {
// Assumes children[0] is the tab list and children[1] is the panels
const tabs = children[0];
const panels = children[1];
// Breaks if someone wraps them in a div or fragment
}Fix: Use compound components with Context (see Example 1) or explicit props:
function Tabs({ tabs, panels }) { ... }See also: React Fundamentals | TypeScript-React | CSS Architecture | Accessibility | Design Systems | UX Patterns
Last reviewed: 2026-02
By Ryan Lind, Assisted by Claude Code and Google Gemini.
TypeScript-React
Type-safe React components — props, generics, discriminated unions, Zod schemas, and utility types for AI-assisted development.
State Management
Server state, client state, URL state, and form state — choosing the right tool for each type in React and Next.js, structured for AI-assisted development.