Overview
Advanced React patterns for production applications — beyond the basics of useState and useEffect.
Compound Components Pattern
Exposes a family of related components that share implicit state through context:
// Context holds shared state
const TabsContext = createContext<TabsContextType | null>(null);
function Tabs({ children, defaultTab }: TabsProps) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function Tab({ id, children }: TabProps) {
const ctx = useContext(TabsContext)!;
return (
<button
className={ctx.activeTab === id ? "active" : ""}
onClick={() => ctx.setActiveTab(id)}
>
{children}
</button>
);
}
// Attach as static properties
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;
Usage: <Tabs defaultTab="a"><Tabs.Tab id="a">Profile</Tabs.Tab></Tabs>
Custom Hooks — Composition Over Inheritance
Extract stateful logic into reusable hooks:
function useAsync<T>(asyncFn: () => Promise<T>, deps: unknown[]) {
const [state, setState] = useState<{
data: T | null;
error: Error | null;
loading: boolean;
}>({ data: null, error: null, loading: true });
useEffect(() => {
setState(s => ({ ...s, loading: true }));
asyncFn()
.then(data => setState({ data, error: null, loading: false }))
.catch(error => setState({ data: null, error, loading: false }));
}, deps);
return state;
}
Memoization Decision Matrix
| Situation | Use | Why |
|-----------|-----|-----|
| Expensive calculation | useMemo | Skip recalculation |
| Callback passed to child component | useCallback | Stable reference |
| Complex child that re-renders often | React.memo | Skip re-render |
| Simple primitive value | Nothing | Memoization has overhead too |
Rule: Profile first. React.memo only pays off when the render is actually expensive or the component is deep in the tree.
React 18 Concurrent Features
useTransition — Non-Urgent Updates
const [isPending, startTransition] = useTransition();
function handleSearch(query: string) {
setInputValue(query); // Urgent: update input immediately
startTransition(() => {
setSearchResults(query); // Non-urgent: can be interrupted
});
}
useDeferredValue — Defer Derived State
const deferredQuery = useDeferredValue(query);
// deferredQuery lags behind query — prevents blocking the input
const results = useMemo(() => search(deferredQuery), [deferredQuery]);
useOptimistic — Instant UI Updates
"use client";
const [optimisticTodos, addOptimistic] = useOptimistic(
todos,
(state, newTodo: Todo) => [...state, newTodo]
);
async function addTodo(formData: FormData) {
const todo = { id: crypto.randomUUID(), title: formData.get("title") as string };
addOptimistic(todo); // Instant UI update
await createTodo(todo); // Server action (actual save)
}
State Management Decision Matrix
| Scope | Tool | When |
|-------|------|------|
| Local UI state | useState | Form inputs, toggles, modals |
| Complex local state | useReducer | State machine, many related fields |
| Cross-component | Context | Theme, auth, locale (low-frequency updates) |
| Server state | React Query / SWR | API data, caching, background refetch |
| Global client state | Zustand | Complex app state, persisted preferences |
Performance Checklist
- Keys on list items are stable IDs, never array indexes
useEffectdependencies are correct and complete- Client components are pushed as low as possible in the tree
- Images use
loading="lazy"or<Image>(Next.js) - Heavy libraries are dynamically imported:
const Chart = dynamic(() => import('./Chart'))