Hooks, Components, State Management, Context API, Performance — everything for modern React.
import { useState } from 'react';
// Basic state
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
// Lazy initializer — expensive computation only runs once
function HeavyInit() {
const [data, setData] = useState(() => {
// This function runs only on the initial render
return expensiveComputation();
});
return <div>{data}</div>;
}
// Object state — merge updates
function UserProfile() {
const [user, setUser] = useState({ name: '', email: '', age: 0 });
const updateName = (name: string) => {
setUser(prev => ({ ...prev, name }));
};
return <div>{user.name}</div>;
}
// Functional updates avoid stale closures
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1); // always gets latest state
}, 1000);
return () => clearInterval(id);
}, []);
return <p>{count}</p>;
}import { useState, useEffect } from 'react';
// Dependency array controls when effect runs
function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// Runs on mount and when userId changes
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (!cancelled) {
setUser(data);
setLoading(false);
}
});
return () => { cancelled = true; };
}, [userId]);
// Runs only on mount (empty deps)
useEffect(() => {
console.log('Component mounted');
return () => console.log('Component unmounted');
}, []);
// Runs after every render (no deps) — avoid!
useEffect(() => {
console.log('Rendered');
});
if (loading) return <p>Loading...</p>;
return <div>{user.name}</div>;
}import { useRef, useState } from 'react';
// Access DOM elements
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} placeholder="Auto-focused" />;
}
// Store mutable value (doesn't trigger re-render)
function Stopwatch() {
const timerRef = useRef<number | null>(null);
const [seconds, setSeconds] = useState(0);
const start = () => {
if (timerRef.current !== null) return;
timerRef.current = window.setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
};
const stop = () => {
if (timerRef.current !== null) {
clearInterval(timerRef.current);
timerRef.current = null;
}
};
return (
<div>
<p>{seconds}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}
// Track previous value
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}import { useState, useCallback, useMemo } from 'react';
// useCallback — memoize function references
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// Recreated only when count changes
const increment = useCallback(() => {
setCount(c => c + 1);
}, []);
// Recreated only when text changes
const clearText = useCallback(() => {
setText('');
}, []);
return (
<div>
<Child onClick={increment} count={count} />
<input value={text} onChange={e => setText(e.target.value)} />
<button onClick={clearText}>Clear</button>
</div>
);
}
const Child = React.memo(
({ onClick, count }: { onClick: () => void; count: number }) => {
console.log('Child rendered');
return (
<button onClick={onClick}>
Count: {count} (memoized)
</button>
);
}
);
// useMemo — memoize expensive computations
function ExpensiveList({ items }: { items: number[] }) {
const sorted = useMemo(
() => [...items].sort((a, b) => a - b),
[items]
);
const total = useMemo(
() => items.reduce((sum, n) => sum + n, 0),
[items]
);
return (
<div>
<p>Total: {total}</p>
<ul>{sorted.map(n => <li key={n}>{n}</li>)}</ul>
</div>
);
}import { createContext, useContext } from 'react';
// Create typed context
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | null>(null);
// Custom hook for consuming context
function useTheme(): ThemeContextType {
const ctx = useContext(ThemeContext);
if (!ctx) {
throw new Error('useTheme must be used within ThemeProvider');
}
return ctx;
}
// Consumer component
function ThemedButton() {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
style={{ background: theme === 'light' ? '#fff' : '#333' }}
>
Current: {theme}
</button>
);
}import {
useActionState,
useOptimistic,
useFormStatus,
use,
useTransition,
useDeferredValue,
} from 'react';
// ── useActionState (React 19) ──
function FormWithActionState() {
const [state, formAction, isPending] = useActionState(
async (prevState: string, formData: FormData) => {
const name = formData.get('name') as string;
await new Promise(r => setTimeout(r, 1000)); // simulate API
return `Hello, ${name}!`;
},
''
);
return (
<form action={formAction}>
<input name="name" />
<button disabled={isPending}>
{isPending ? 'Submitting...' : 'Submit'}
</button>
<p>{state}</p>
</form>
);
}
// ── useOptimistic (React 19) ──
function LikeButton({ postId }: { postId: number }) {
const [likes, setLikes] = useState(42);
const [optimisticLikes, addOptimistic] = useOptimistic(
likes,
(state, newLikes: number) => state + newLikes
);
async function handleLike() {
addOptimistic(1);
await fetch(`/api/posts/${postId}/like`, { method: 'POST' });
setLikes(l => l + 1);
}
return <button onClick={handleLike}>❤️ {optimisticLikes}</button>;
}
// ── useFormStatus (React 19) ──
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? 'Saving...' : 'Save'}</button>;
}
// ── use() — read resources (promises, context) ──
function MessageComponent({ messagePromise }: { messagePromise: Promise<string> }) {
const message = use(messagePromise);
return <p>{message}</p>;
}
// ── useTransition — non-blocking state updates
function SearchPage() {
const [isPending, startTransition] = useTransition();
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value); // urgent: update input
startTransition(() => {
setResults(searchResults(e.target.value)); // non-urgent
});
};
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <p>Searching...</p>}
<ResultList results={results} />
</div>
);
}
// ── useDeferredValue — defer expensive updates
function DeferredSearch({ query }: { query: string }) {
const deferredQuery = useDeferredValue(query);
const suggestions = useMemo(
() => computeSuggestions(deferredQuery),
[deferredQuery]
);
return <ul>{suggestions.map(s => <li key={s}>{s}</li>)}</ul>;
}import { useReducer } from 'react';
// Complex state with useReducer
interface State {
count: number;
step: number;
}
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'setStep'; payload: number }
| { type: 'reset' };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + state.step };
case 'decrement':
return { ...state, count: state.count - state.step };
case 'setStep':
return { ...state, step: action.payload };
case 'reset':
return { count: 0, step: 1 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, {
count: 0,
step: 1,
});
return (
<div>
<p>Count: {state.count}</p>
<input
type="number"
value={state.step}
onChange={e =>
dispatch({ type: 'setStep', payload: +e.target.value })
}
/>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
);
}import { useState, useEffect, useCallback } from 'react';
// Reusable useLocalStorage hook
function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] {
const [stored, setStored] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback(
(value: T | ((prev: T) => T)) => {
setStored(prev => {
const next = value instanceof Function ? value(prev) : value;
window.localStorage.setItem(key, JSON.stringify(next));
return next;
});
},
[key]
);
return [stored, setValue];
}
// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'dark');
return (
<button onClick={() =>
setTheme(t => t === 'dark' ? 'light' : 'dark')
}>
{theme}
</button>
);
}
// useDebounce hook
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// useFetch hook
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(url)
.then(res => res.json())
.then(d => { if (!cancelled) setData(d); })
.catch(e => { if (!cancelled) setError(e.message); })
.finally(() => { if (!cancelled) setLoading(false); });
return () => { cancelled = true; };
}, [url]);
return { data, error, loading };
}| Hook | Purpose | Re-renders? | Use Case |
|---|---|---|---|
| useState | Local component state | Yes | Simple values, toggles, inputs |
| useEffect | Side effects | No | Data fetching, subscriptions, DOM |
| useRef | Mutable ref / DOM access | No | Focus, intervals, previous value |
| useCallback | Memoize function | No | Pass to memoized children |
| useMemo | Memoize computation | No | Expensive derived values |
| useContext | Consume context | Yes | Global state (theme, auth) |
| useReducer | Complex state logic | Yes | Multi-field forms, state machines |
| useTransition | Non-blocking updates | No | Filtering, large list updates |
| useDeferredValue | Defer rendering | No | Search suggestions |
| useActionState | Form + async action | Yes | Server actions, form submission |
| useOptimistic | Optimistic UI | Yes | Likes, toggles with pending state |
| useFormStatus | Form pending state | Yes | Submit button in forms |
| use | Read promises/context | Yes | Suspense data, context in render |
| useId | Unique IDs | No | Accessible form labels, aria |
useActionState replaces useState+useTransition for form actions. useOptimistic lets you show immediate UI while async operations complete. useFormStatus gives pending state from a parent <form>.// Function component with typed props
interface GreetingProps {
name: string;
age?: number; // optional
children?: React.ReactNode;
}
// Regular props
function Greeting({ name, age }: GreetingProps) {
return <h1>Hello, {name}! {age && `(${age}y)`}</h1>;
}
// Props with default values
function Button({
label,
variant = 'primary',
size = 'md',
disabled = false,
}: {
label: string;
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
}) {
return (
<button className={`btn btn-${variant} btn-${size}`}
disabled={disabled}>
{label}
</button>
);
}
// Destructuring with rest props
function Card({ title, children, ...rest }: {
title: string;
children: React.ReactNode;
} & React.HTMLAttributes<HTMLDivElement>) {
return (
<div {...rest} className="card">
<h3>{title}</h3>
{children}
</div>
);
}// Children prop — composition over configuration
interface LayoutProps {
children: React.ReactNode;
header?: React.ReactNode;
footer?: React.ReactNode;
}
function Layout({ children, header, footer }: LayoutProps) {
return (
<div className="layout">
{header && <header>{header}</header>}
<main>{children}</main>
{footer && <footer>{footer}</footer>}
</div>
);
}
// Usage — declarative composition
function App() {
return (
<Layout
header={<nav><Logo /> <NavLinks /></nav>}
footer={<p>© 2026</p>}
>
<HomePage />
</Layout>
);
}
// Function as children (render props pattern)
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return (
<ul>
{items.map((item, i) => (
<li key={i}>{renderItem(item, i)}</li>
))}
</ul>
);
}
// Usage
<List
items={users}
renderItem={(user) => (
<span>{user.name} — {user.email}</span>
)}
/>import { Fragment, createPortal } from 'react';
// ── Fragments — group elements without extra DOM nodes ──
// Short syntax: <>
function Table({ rows }: { rows: string[][] }) {
return (
<table>
<tbody>
{rows.map((row, i) => (
<tr key={i}>
{row.map((cell, j) => (
<td key={j}>{cell}</td>
))}
</tr>
))}
</tbody>
</table>
);
}
// Explicit Fragment (needed when passing key)
function Columns({ items }: { items: { id: number; text: string }[] }) {
return (
<>
{items.map(item => (
<Fragment key={item.id}>
<dt>{item.text}</dt>
<dd>Description for {item.text}</dd>
</Fragment>
))}
</>
);
}
// ── Portals — render into DOM nodes outside parent hierarchy ──
function Modal({ isOpen, children }: {
isOpen: boolean;
children: React.ReactNode;
}) {
if (!isOpen) return null;
return createPortal(
<div className="modal-overlay">
<div className="modal-content">{children}</div>
</div>,
document.getElementById('portal-root')!
);
}
// Common portal targets: modals, tooltips, notifications
function Toast({ message }: { message: string }) {
return createPortal(
<div className="toast">{message}</div>,
document.getElementById('toast-container')!
);
}import { forwardRef, lazy, Suspense } from 'react';
// ── forwardRef — pass ref to child components ──
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
error?: string;
}
const FancyInput = forwardRef<HTMLInputElement, InputProps>(
({ label, error, ...props }, ref) => {
return (
<div className="input-group">
<label>{label}</label>
<input ref={ref} {...props} className={error ? 'error' : ''} />
{error && <span className="error-text">{error}</span>}
</div>
);
}
);
FancyInput.displayName = 'FancyInput';
// Usage with ref
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
const handleSubmit = () => {
inputRef.current?.focus();
};
return (
<form>
<FancyInput ref={inputRef} label="Email" type="email" />
<button type="button" onClick={handleSubmit}>Submit</button>
</form>
);
}
// ── React.lazy + Suspense — code splitting ──
const HeavyChart = lazy(() => import('./HeavyChart'));
const SettingsPanel = lazy(() => import('./SettingsPanel'));
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading chart...</div>}>
<HeavyChart />
</Suspense>
<Suspense fallback={<div>Loading settings...</div>}>
<SettingsPanel />
</Suspense>
</div>
);
}import { Component, ReactNode } from 'react';
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, info: React.ErrorInfo) => void;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error('ErrorBoundary caught:', error, info);
this.props.onError?.(error, info);
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? (
<div className="error-fallback">
<h2>Something went wrong</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false, error: null })}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
// Usage — wrap any component tree
function App() {
return (
<ErrorBoundary fallback={<p>This section crashed.</p>}>
<RiskyComponent />
</ErrorBoundary>
);
}| Pattern | When to Use | Key Benefit |
|---|---|---|
| Function Component | Default choice | Simple, hooks, composable |
| forwardRef | Pass refs to children | Imperative DOM access |
| lazy/Suspense | Large routes/charts | Code splitting, faster loads |
| Error Boundary | Unrecoverable errors | Graceful error handling |
| Portal | Modals, tooltips | Escape CSS/scoping |
| HOC | Legacy codebases | Cross-cutting concerns |
| Render Props | Share render logic | Flexibility in rendering |
Fragment with key prop when rendering lists of elements without a wrapping DOM node. The shorthand <></> syntax does not support keys.import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
// ── Basic store ──
interface CartStore {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
clearCart: () => void;
total: () => number;
}
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
const useCartStore = create<CartStore>()(
devtools(
persist(
(set, get) => ({
items: [],
addItem: (item) =>
set((state) => ({
items: [...state.items, item],
})),
removeItem: (id) =>
set((state) => ({
items: state.items.filter((i) => i.id !== id),
})),
clearCart: () => set({ items: [] }),
total: () =>
get().items.reduce(
(sum, i) => sum + i.price * i.quantity, 0
),
}),
{ name: 'cart-storage' } // localStorage key
),
{ name: 'CartStore' } // DevTools name
)
);
// ── Usage in components ──
function CartButton({ item }: { item: CartItem }) {
const addItem = useCartStore((s) => s.addItem);
return <button onClick={() => addItem(item)}>Add to Cart</button>;
}
function CartSummary() {
const items = useCartStore((s) => s.items);
const total = useCartStore((s) => s.total);
return (
<div>
{items.map((i) => <p key={i.id}>{i.name}: ${i.price}</p>)}
<p>Total: ${total()}</p>
</div>
);
}import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
// ── Primitive atoms ──
const countAtom = atom(0);
const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'dark');
// ── Derived atoms (read-only) ──
const doubledAtom = atom((get) => get(countAtom) * 2);
// ── Write-only atoms ──
const incrementAtom = atom(null, (get, set) => {
set(countAtom, get(countAtom) + 1);
});
// ── Async atom ──
const userDataAtom = atom(async (get) => {
const userId = get(userIdAtom);
const res = await fetch(`/api/users/${userId}`);
return res.json();
});
// ── Usage ──
function Counter() {
const [count, setCount] = useAtom(countAtom);
const doubled = useAtomValue(doubledAtom);
const inc = useSetAtom(incrementAtom);
return (
<div>
<p>Count: {count}, Doubled: {doubled}</p>
<button onClick={() => setCount((c) => c + 1)}>+</button>
<button onClick={inc}>Increment via atom</button>
</div>
);
}
function ThemeToggle() {
const [theme, setTheme] = useAtom(themeAtom);
return (
<button onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}>
{theme}
</button>
);
}import { useState, useActionState } from 'react';
// ── Controlled inputs ──
function ControlledForm() {
const [form, setForm] = useState({
email: '',
password: '',
remember: false,
});
const [errors, setErrors] = useState<Record<string, string>>({});
const handleChange = (
e: React.ChangeEvent<HTMLInputElement>
) => {
const { name, value, type, checked } = e.target;
setForm(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value,
}));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!form.email.includes('@')) {
setErrors({ email: 'Invalid email' });
return;
}
console.log('Submit:', form);
};
return (
<form onSubmit={handleSubmit}>
<input name="email" value={form.email}
onChange={handleChange} />
{errors.email && <span>{errors.email}</span>}
<input name="password" type="password"
value={form.password} onChange={handleChange} />
<label>
<input name="remember" type="checkbox"
checked={form.remember} onChange={handleChange} />
Remember me
</label>
<button type="submit">Sign In</button>
</form>
);
}
// ── React 19 useActionState for forms ──
async function submitForm(
_prevState: { error: string; success: boolean },
formData: FormData
) {
const email = formData.get('email') as string;
const password = formData.get('password') as string;
if (!email || !password) {
return { error: 'All fields required', success: false };
}
await new Promise(r => setTimeout(r, 1000));
return { error: '', success: true };
}
function ModernForm() {
const [state, formAction, isPending] = useActionState(
submitForm,
{ error: '', success: false }
);
return (
<form action={formAction}>
<input name="email" type="email" placeholder="Email" />
<input name="password" type="password" placeholder="Password" />
<button disabled={isPending}>
{isPending ? 'Loading...' : 'Sign In'}
</button>
{state.error && <p style={{ color: 'red' }}>{state.error}</p>}
{state.success && <p style={{ color: 'green' }}>Success!</p>}
</form>
);
}import { useReducer } from 'react';
// ── State machine pattern for complex UI state ──
type Status = 'idle' | 'loading' | 'success' | 'error';
interface FetchState<T> {
status: Status;
data: T | null;
error: string | null;
}
type FetchAction<T> =
| { type: 'FETCH_START' }
| { type: 'FETCH_SUCCESS'; payload: T }
| { type: 'FETCH_ERROR'; error: string }
| { type: 'RESET' };
function fetchReducer<T>(state: FetchState<T>, action: FetchAction<T>): FetchState<T> {
switch (action.type) {
case 'FETCH_START':
return { status: 'loading', data: null, error: null };
case 'FETCH_SUCCESS':
return { status: 'success', data: action.payload, error: null };
case 'FETCH_ERROR':
return { status: 'error', data: null, error: action.error };
case 'RESET':
return { status: 'idle', data: null, error: null };
default:
return state;
}
}
function UserList() {
const [state, dispatch] = useReducer(
fetchReducer<User[]>,
{ status: 'idle', data: null, error: null }
);
const loadUsers = async () => {
dispatch({ type: 'FETCH_START' });
try {
const res = await fetch('/api/users');
const data = await res.json();
dispatch({ type: 'FETCH_SUCCESS', payload: data });
} catch (err) {
dispatch({ type: 'FETCH_ERROR', error: 'Failed to load' });
}
};
return (
<div>
<button onClick={loadUsers}>Load Users</button>
{state.status === 'loading' && <p>Loading...</p>}
{state.status === 'error' && <p>{state.error}</p>}
{state.status === 'success' && (
<ul>{state.data!.map(u => <li key={u.id}>{u.name}</li>)}</ul>
)}
</div>
);
}| Solution | Scale | Bundle Size | Best For |
|---|---|---|---|
| useState | Component | 0 KB (built-in) | Simple local state |
| useReducer | Component | 0 KB (built-in) | Complex component state |
| Context API | App-wide | 0 KB (built-in) | Theme, locale, auth |
| Zustand | App-wide | ~1.2 KB | Global stores, middleware |
| Jotai | Atomic | ~3 KB | Fine-grained reactive atoms |
| Redux Toolkit | Large apps | ~12 KB | Enterprise, devtools |
| TanStack Query | Server | ~12 KB | Server state, caching |
import { createContext, useContext, useState, useCallback } from 'react';
// ── Step 1: Define context shape ──
interface AuthContextType {
user: User | null;
isAuthenticated: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
loading: boolean;
}
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
// Step 2: Create context with null default
const AuthContext = createContext<AuthContextType | null>(null);
// Step 3: Create provider component
function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const login = useCallback(async (email: string, password: string) => {
setLoading(true);
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.message);
setUser(data.user);
setLoading(false);
}, []);
const logout = useCallback(() => {
setUser(null);
document.cookie = 'token=; max-age=0';
}, []);
const value: AuthContextType = {
user,
isAuthenticated: !!user,
login,
logout,
loading,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
// Step 4: Create custom hook
function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
// Step 5: Wrap app
function App() {
return (
<AuthProvider>
<Router>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Router>
</AuthProvider>
);
}import { createContext, useContext, useState, useMemo } from 'react';
// ── Problem: Single context causes unnecessary re-renders ──
// When ANY part of context changes, ALL consumers re-render.
// Split into multiple contexts to avoid this.
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
interface UserContextType {
user: User | null;
setUser: (user: User | null) => void;
}
// Split into separate contexts
const ThemeContext = createContext<ThemeContextType | null>(null);
const UserContext = createContext<UserContextType | null>(null);
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('dark');
const value = useMemo(
() => ({
theme,
toggleTheme: () => setTheme(t => t === 'dark' ? 'light' : 'dark'),
}),
[theme]
);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
function UserProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
// Now theme changes don't re-render user consumers and vice versa
function ThemeAwareComponent() {
const { theme } = useContext(ThemeContext);
return <div data-theme={theme}>Themed content</div>;
}
function UserAwareComponent() {
const { user } = useContext(UserContext);
return <div>{user?.name ?? 'Not logged in'}</div>;
}// ── Compose multiple providers ──
function AppProviders({ children }: { children: React.ReactNode }) {
return (
<AuthProvider>
<ThemeProvider>
<LocaleProvider>
<NotificationProvider>
{children}
</NotificationProvider>
</LocaleProvider>
</ThemeProvider>
</AuthProvider>
);
}
// Or use a helper to flatten nesting
function composeProviders(
providers: React.ComponentType<{ children: React.ReactNode }>[],
children: React.ReactNode
) {
return providers.reduceRight(
(child, Provider) => <Provider>{child}</Provider>,
children
);
}
// Usage
function App() {
return (
{composeProviders(
[AuthProvider, ThemeProvider, LocaleProvider, NotificationProvider],
<Router>{/* routes */}</Router>
)}
);
}
// ── Context selector pattern (similar to Redux useSelector) ──
import { createContext, useContext, useState } from 'react';
function createContextSelector<T>() {
const Context = createContext<T | null>(null);
function useContextSelector(): T;
function useContextSelector<K>(selector: (value: T) => K): K;
function useContextSelector(selector?: any) {
const context = useContext(Context);
if (!context) throw new Error('Must be used within provider');
return selector ? selector(context) : context;
}
return { Context, useContextSelector };
}
// Create and use
const { Context: StoreContext, useContextSelector: useStore } =
createContextSelector<AppState>();
function UserName() {
const name = useStore(state => state.user.name);
return <span>{name}</span>; // only re-renders when user.name changes
}| Context | Data Shape | Frequency |
|---|---|---|
| ThemeContext | { theme, toggle } | Toggle rarely |
| AuthContext | { user, login, logout } | On auth change |
| LocaleContext | { locale, setLocale } | On locale change |
| NotificationContext | { toasts, addToast } | Frequent adds |
| FeatureFlags | { flags, isEnabled } | On config load |
useMemo, and split contexts by concern to minimize impact.import { memo, useState } from 'react';
// ── React.memo — skip re-render if props are unchanged ──
interface UserCardProps {
user: { id: string; name: string; avatar: string };
onFollow: (id: string) => void;
isSelected: boolean;
}
const UserCard = memo(function UserCard({
user,
onFollow,
isSelected,
}: UserCardProps) {
console.log(`Rendering ${user.name}`);
return (
<div className={isSelected ? 'selected' : ''}>
<img src={user.avatar} alt={user.name} />
<h3>{user.name}</h3>
<button onClick={() => onFollow(user.id)}>
Follow
</button>
</div>
);
});
// memo with custom comparison
const ExpensiveList = memo(
({ items, filter }: { items: Item[]; filter: string }) => {
return items
.filter(i => i.name.includes(filter))
.map(i => <div key={i.id}>{i.name}</div>);
},
(prevProps, nextProps) => {
// Return true if props are equal (skip re-render)
return prevProps.filter === nextProps.filter
&& prevProps.items.length === nextProps.items.length;
}
);
// When to use memo:
// - Component renders often with same props
// - Component has expensive render
// - Used as list item in large lists
// - Referentially stable callbacks are passed (useCallback)
function UserList() {
const [users, setUsers] = useState<User[]>([]);
const [search, setSearch] = useState('');
const [selected, setSelected] = useState<string | null>(null);
// useCallback so onFollow doesn't break memo
const handleFollow = useCallback((id: string) => {
console.log('Follow:', id);
}, []);
return (
<div>
<input value={search} onChange={e => setSearch(e.target.value)} />
{users.map(u => (
<UserCard
key={u.id}
user={u}
onFollow={handleFollow}
isSelected={selected === u.id}
/>
))}
</div>
);
}import { useMemo, useCallback } from 'react';
// ── useMemo: cache expensive computations ──
function ProductList({ products, sortBy, filterFn }: {
products: Product[];
sortBy: 'price' | 'name' | 'rating';
filterFn: (p: Product) => boolean;
}) {
// Only recomputes when dependencies change
const filtered = useMemo(
() => products.filter(filterFn),
[products, filterFn]
);
const sorted = useMemo(
() => [...filtered].sort((a, b) => {
if (sortBy === 'price') return a.price - b.price;
if (sortBy === 'name') return a.name.localeCompare(b.name);
return b.rating - a.rating;
}),
[filtered, sortBy]
);
return (
<ul>
{sorted.map(p => (
<li key={p.id}>{p.name} — ${p.price}</li>
))}
</ul>
);
}
// ── useCallback: stable function references ──
function SearchBar({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('');
// Without useCallback, new function on every render
const handleSubmit = useCallback(() => {
onSearch(query);
}, [query, onSearch]);
// With useCallback + debounce
return (
<input
value={query}
onChange={e => setQuery(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleSubmit()}
/>
);
}import { useVirtualizer } from '@tanstack/react-virtual';
// ── Virtualized list — only render visible items ──
function VirtualList({ items }: { items: string[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const rowVirtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 48, // estimated row height in px
overscan: 5, // render extra rows above/below viewport
});
return (
<div ref={parentRef} style={{ height: '500px', overflow: 'auto' }}>
<div
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
<div
key={virtualRow.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
}}
>
<div style={{ padding: '8px 16px' }}>
Row {virtualRow.index}: {items[virtualRow.index]}
</div>
</div>
))}
</div>
</div>
);
}
// Handles 100,000+ items smoothly
function App() {
const items = Array.from({ length: 100_000 }, (_, i) =>
`Item ${i + 1}`
);
return <VirtualList items={items} />;
}import { lazy, Suspense } from 'react';
// ── Route-level code splitting ──
import dynamic from 'next/dynamic';
// Next.js dynamic import with loading state
const AdminDashboard = dynamic(
() => import('@/components/AdminDashboard'),
{
loading: () => <Skeleton />,
ssr: false, // client-only component
}
);
const HeavyChart = dynamic(
() => import('@/components/Chart'),
{ loading: () => <ChartSkeleton /> }
);
// React.lazy (vanilla React)
const Modal = lazy(() => import('./Modal'));
const RichTextEditor = lazy(() => import('./RichTextEditor'));
// ── Conditional loading ──
function Dashboard({ showChart }: { showChart: boolean }) {
return (
<div>
<h1>Dashboard</h1>
{showChart && (
<Suspense fallback={<div>Loading chart...</div>}>
<HeavyChart />
</Suspense>
)}
</div>
);
}
// ── Bundle analysis (terminal) ──
// npm install -D @next/bundle-analyzer
// next.config.js:
// const withBundleAnalyzer = require('@next/bundle-analyzer')({
// enabled: process.env.ANALYZE === 'true',
// });
// module.exports = withBundleAnalyzer({ /* config */ });| Anti-Pattern | Problem | Fix |
|---|---|---|
| Inline objects in props | New ref every render | useMemo |
| Inline functions in props | Breaks memo() | useCallback |
| useMemo on everything | Memoization overhead | Only expensive ops |
| useState for derived data | Sync state duplication | Compute on render |
| Context for frequent updates | All consumers re-render | Zustand / Jotai |
| Large unsplit bundles | Slow initial load | Code splitting |
| No loading states | Perceived slowness | Suspense / skeletons |
memo, useMemo, and useCallback when profiling shows actual performance issues. Premature optimization adds complexity without measurable benefit.import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import { Counter } from './Counter';
describe('Counter', () => {
it('renders initial count', () => {
render(<Counter initialCount={5} />);
expect(screen.getByText('Count: 5')).toBeInTheDocument();
});
it('increments on button click', async () => {
const user = userEvent.setup();
render(<Counter initialCount={0} />);
const button = screen.getByRole('button', { name: /increment/i });
await user.click(button);
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
it('calls onChange callback', async () => {
const user = userEvent.setup();
const onChange = vi.fn();
render(<Counter initialCount={0} onChange={onChange} />);
await user.click(screen.getByRole('button', { name: /increment/i }));
expect(onChange).toHaveBeenCalledWith(1);
});
});import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { useCounter, useLocalStorage, useFetch } from './hooks';
describe('useCounter', () => {
it('initializes with default value', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
it('increments', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(6);
});
it('decrements', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(4);
});
it('resets to initial value', () => {
const { result } = renderHook(() => useCounter(10));
act(() => { result.current.increment(); result.current.increment(); });
act(() => { result.current.reset(); });
expect(result.current.count).toBe(10);
});
});
describe('useLocalStorage', () => {
beforeEach(() => {
localStorage.clear();
});
it('reads initial value', () => {
const { result } = renderHook(() => useLocalStorage('key', 'default'));
expect(result.current[0]).toBe('default');
});
it('persists value', () => {
const { result } = renderHook(() => useLocalStorage('key', 'default'));
act(() => { result.current[1]('updated'); });
expect(localStorage.getItem('key')).toBe('"updated"');
});
it('reads existing value from storage', () => {
localStorage.setItem('key', '"stored"');
const { result } = renderHook(() => useLocalStorage('key', 'default'));
expect(result.current[0]).toBe('stored');
});
});import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
import { UserProfile } from './UserProfile';
// ── Mock Service Worker (MSW) setup ──
const server = setupServer(
http.get('/api/users/1', () => {
return HttpResponse.json({
id: '1',
name: 'Jane Doe',
email: 'jane@example.com',
});
}),
http.post('/api/users', async ({ request }) => {
const body = await request.json();
return HttpResponse.json(body, { status: 201 });
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('UserProfile', () => {
it('loads and displays user data', async () => {
render(<UserProfile userId="1" />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('Jane Doe')).toBeInTheDocument();
});
});
it('handles server errors', async () => {
server.use(
http.get('/api/users/1', () => {
return new HttpResponse(null, { status: 500 });
})
);
render(<UserProfile userId="1" />);
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument();
});
});
});// ── Vitest setup file (vitest-setup.ts) ──
import '@testing-library/jest-dom/vitest';
// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
// Mock IntersectionObserver
class MockIntersectionObserver {
observe = vi.fn();
unobserve = vi.fn();
disconnect = vi.fn();
}
Object.defineProperty(window, 'IntersectionObserver', {
writable: true,
value: MockIntersectionObserver,
});
// Mock ResizeObserver
class MockResizeObserver {
observe = vi.fn();
unobserve = vi.fn();
disconnect = vi.fn();
}
Object.defineProperty(window, 'ResizeObserver', {
writable: true,
value: MockResizeObserver,
});| Query Method | Scope | Use For |
|---|---|---|
| getByRole | ARIA roles | Buttons, links, headings |
| getByText | Text content | Labels, paragraphs |
| getByLabelText | Form labels | Inputs with labels |
| getByPlaceholderText | Placeholder | Input placeholders |
| getByTestId | data-testid | Last resort fallback |
| getAllBy... | Multiple | Lists, tables |
| queryBy... | Not found = null | Check absence |
| findBy... | Async/Await | Data loading |
userEvent from @testing-library/user-event — it simulates real browser behavior (click, type, hover) including event propagation, focus management, and keyboard interactions.// ── Compound Component Pattern ──
// Components that work together to form a complete UI
import { createContext, useContext, useState, ReactNode } from 'react';
// Context for sharing state between compound parts
interface AccordionContextType {
openItems: Set<string>;
toggle: (id: string) => void;
}
const AccordionCtx = createContext<AccordionContextType | null>(null);
function useAccordion() {
const ctx = useContext(AccordionCtx);
if (!ctx) throw new Error('Must be used within Accordion');
return ctx;
}
// Root component — manages state
function Accordion({ children, defaultOpen = [] }: {
children: ReactNode;
defaultOpen?: string[];
}) {
const [openItems, setOpenItems] = useState(new Set(defaultOpen));
const toggle = (id: string) => {
setOpenItems(prev => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
};
return (
<AccordionCtx.Provider value={{ openItems, toggle }}>
<div className="accordion">{children}</div>
</AccordionCtx.Provider>
);
}
// Item component
function AccordionItem({ id, children }: {
id: string;
children: ReactNode;
}) {
const { openItems } = useAccordion();
const isOpen = openItems.has(id);
return (
<div className={`accordion-item ${isOpen ? 'open' : ''}`}>
{children}
</div>
);
}
// Trigger component
function AccordionTrigger({ id, children }: {
id: string;
children: ReactNode;
}) {
const { toggle } = useAccordion();
return (
<button className="accordion-trigger" onClick={() => toggle(id)}>
{children}
</button>
);
}
// Content component
function AccordionContent({ id, children }: {
id: string;
children: ReactNode;
}) {
const { openItems } = useAccordion();
if (!openItems.has(id)) return null;
return <div className="accordion-content">{children}</div>;
}
// Usage — declarative and flexible
function FAQ() {
return (
<Accordion defaultOpen={['what-is-react']}>
<AccordionItem id="what-is-react">
<AccordionTrigger id="what-is-react">
What is React?
</AccordionTrigger>
<AccordionContent id="what-is-react">
A JavaScript library for building user interfaces.
</AccordionContent>
</AccordionItem>
<AccordionItem id="what-are-hooks">
<AccordionTrigger id="what-are-hooks">
What are Hooks?
</AccordionTrigger>
<AccordionContent id="what-are-hooks">
Functions that let you use state in function components.
</AccordionContent>
</AccordionItem>
</Accordion>
);
}// ── Higher-Order Component (HOC) Pattern ──
// Wraps a component to add extra behavior
// withAuth — protects routes
function withAuth<P extends object>(
Component: React.ComponentType<P>
) {
return function AuthenticatedComponent(props: P) {
const { user, loading } = useAuth();
if (loading) return <LoadingSpinner />;
if (!user) return <Navigate to="/login" />;
return <Component {...props} />;
};
}
// Usage
const ProtectedDashboard = withAuth(Dashboard);
// withLoading — adds loading state
function withLoading<P extends { isLoading?: boolean }>(
Component: React.ComponentType<P>,
LoadingComponent: React.ComponentType = () => <Skeleton />
) {
return function WithLoading(props: P) {
const { isLoading, ...rest } = props;
if (isLoading) return <LoadingComponent />;
return <Component {...(rest as P)} />;
};
}
// Usage
const UserProfileWithLoading = withLoading(UserProfile);
// withLogger — logs lifecycle events
function withLogger<P extends object>(
Component: React.ComponentType<P>,
name: string
) {
return function LoggedComponent(props: P) {
useEffect(() => {
console.log(`[${name}] mounted`);
return () => console.log(`[${name}] unmounted`);
}, []);
return <Component {...props} />;
};
}// ── Render Props Pattern ──
// Pass rendering logic as a function prop
// Hover detector
function HoverDetector({ children }: {
children: (isHovered: boolean) => ReactNode;
}) {
const [isHovered, setIsHovered] = useState(false);
return (
<div
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{children(isHovered)}
</div>
);
}
// Usage
function TooltipButton() {
return (
<HoverDetector>
{(isHovered) => (
<button style={{ background: isHovered ? '#333' : '#111' }}>
{isHovered ? 'Hovering!' : 'Hover me'}
</button>
)}
</HoverDetector>
);
}
// List with render prop
function DataSource<T>({ getData, renderItem }: {
getData: () => T[];
renderItem: (item: T, index: number) => ReactNode;
}) {
const data = getData();
return <ul>{data.map((item, i) => <li key={i}>{renderItem(item, i)}</li>)}</ul>;
}
// Custom hook alternative (preferred over render props in modern React)
function useHover() {
const [isHovered, setIsHovered] = useState(false);
const handlers = {
onMouseEnter: () => setIsHovered(true),
onMouseLeave: () => setIsHovered(false),
};
return { isHovered, ...handlers };
}
// Usage — cleaner than render props
function HoverableCard() {
const { isHovered, ...handlers } = useHover();
return (
<div {...handlers} style={{ opacity: isHovered ? 1 : 0.7 }}>
<h3>Card</h3>
</div>
);
}// ── Controlled vs Uncontrolled Components ──
// Controlled — React owns the state
function ControlledInput() {
const [value, setValue] = useState('');
return (
<input
value={value}
onChange={e => setValue(e.target.value)}
/>
);
}
// Uncontrolled — DOM owns the state
function UncontrolledInput() {
const inputRef = useRef<HTMLInputElement>(null);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log(inputRef.current?.value); // read from DOM
};
return (
<form onSubmit={handleSubmit}>
<input ref={inputRef} defaultValue="Hello" />
<button type="submit">Submit</button>
</form>
);
}
// ── Custom hook for form management ──
function useForm<T extends Record<string, any>>(initialValues: T) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setValues(prev => ({ ...prev, [name]: value }));
// Clear error on change
if (errors[name as keyof T]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
};
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
const { name } = e.target;
setTouched(prev => ({ ...prev, [name]: true }));
};
const validate = (rules: Record<keyof T, (val: any) => string | undefined>) => {
const newErrors: Partial<Record<keyof T, string>> = {};
for (const key in rules) {
const error = rules[key](values[key]);
if (error) newErrors[key] = error;
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const reset = () => {
setValues(initialValues);
setErrors({});
setTouched({});
};
return { values, errors, touched, handleChange, handleBlur, validate, reset, setValues };
}// ── Provider + Custom Hook Pattern ──
// Encapsulate state + logic, expose clean API
interface ModalContextType {
isOpen: boolean;
content: ReactNode | null;
open: (content: ReactNode) => void;
close: () => void;
}
const ModalContext = createContext<ModalContextType | null>(null);
function ModalProvider({ children }: { children: ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
const [content, setContent] = useState<ReactNode | null>(null);
const open = useCallback((node: ReactNode) => {
setContent(node);
setIsOpen(true);
}, []);
const close = useCallback(() => {
setIsOpen(false);
// Delay clearing content for close animation
setTimeout(() => setContent(null), 200);
}, []);
return (
<ModalContext.Provider value={{ isOpen, content, open, close }}>
{children}
{isOpen && (
<div className="modal-overlay" onClick={close}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
{content}
<button onClick={close}>Close</button>
</div>
</div>
)}
</ModalContext.Provider>
);
}
function useModal() {
const ctx = useContext(ModalContext);
if (!ctx) throw new Error('useModal must be within ModalProvider');
return ctx;
}
// Usage — any component can open a modal
function DeleteButton({ itemId }: { itemId: string }) {
const { open } = useModal();
return (
<button onClick={() =>
open(
<div>
<h3>Delete item?</h3>
<p>This cannot be undone.</p>
<button onClick={() => deleteItem(itemId)}>Confirm</button>
</div>
)
}>
Delete
</button>
);
}| Pattern | Type | Modern? | Use Case |
|---|---|---|---|
| Compound Components | Composition | Yes | Accordions, tabs, menus |
| Render Props | Inversion of control | Replaced | Share render logic |
| HOCs | Wrapping | Legacy | Auth, logging, data injection |
| Custom Hooks | Logic extraction | Best | Reuse stateful logic |
| Provider + Hook | State + API | Best | Modals, toasts, auth |
| Controlled/Uncontrolled | Form patterns | Both | Form inputs, file uploads |
React maintains a lightweight JavaScript copy of the real DOM called the Virtual DOM. When state changes, React creates a new VDOM tree, compares it with the previous one (diffing), and calculates the minimal set of real DOM operations needed (reconciliation). This batching avoids costly direct DOM manipulations.
The diffing algorithm uses heuristic O(n) comparison: it assumes elements of the same type at the same position produce similar output. Keys help it identify which children changed, moved, or were added/removed across renders.
| Step | Description | Performance |
|---|---|---|
| 1. Render | Create new VDOM tree from JSX | Fast (JS objects) |
| 2. Diff | Compare new VDOM vs previous VDOM | O(n) heuristic |
| 3. Reconcile | Compute minimal DOM mutations | Fast diff algorithm |
| 4. Commit | Apply changes to real DOM in batch | Single reflow/repaint |
Use useState for simple, independent state values (toggles, strings, simple objects). Use useReducer when state logic is complex, involves multiple sub-values, depends on previous state, or when the next state depends on the current one in intricate ways.
| Criteria | useState | useReducer |
|---|---|---|
| State complexity | Simple values | Complex objects / multiple fields |
| Update logic | Simple set | Complex transitions / actions |
| Related state | Independent | Multiple interdependent fields |
| Testability | Test component | Test reducer function independently |
| Middleware | N/A | Can integrate middleware patterns |
| Typical use | Toggle, input, counter | Forms, state machines, wizards |
useReducer.The cleanup function returned from useEffectruns before the effect re-runs and before the component unmounts. It's essential for:
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') setError(err.message);
});
// Cleanup: abort fetch if component unmounts
// or if effect re-runs (deps change)
return () => controller.abort();
}, [userId]);// Problem: new object + function on every render
function Parent() {
const [count, setCount] = useState(0);
return (
<Child
style={{ color: 'red' }} // new object every render
onClick={() => setCount(c => c + 1)} // new function every render
/>
);
}
// Solution: memoize
function Parent() {
const [count, setCount] = useState(0);
const style = useMemo(() => ({ color: 'red' }), []);
const onClick = useCallback(() => setCount(c => c + 1), []);
return <Child style={style} onClick={onClick} />;
}React Server Components render on the server and send HTML to the client. They can directly access databases, file systems, and environment variables without API routes.
| Feature | Server Component | Client Component |
|---|---|---|
| Render location | Server only | Browser (or SSR hydration) |
| Directives | Default in Next.js App Router | 'use client' |
| State/hooks | No useState, useEffect, etc. | All hooks available |
| Event handlers | Cannot use onClick, onChange | Full event handling |
| Browser APIs | Cannot access window, document | Full access |
| Bundle size | Zero JS sent to client | Included in bundle |
| Data fetching | Direct async/await | useEffect / SWR / TanStack Query |
| Best for | Data display, layouts, SEO | Interactivity, forms, state |
'use client' only when you need hooks, event handlers, or browser APIs. Keep client boundaries as small as possible.Fiber is React's reconciliation engine (React 16+). It breaks rendering work into small units called fibers, each representing a component instance. This enables incremental rendering — React can pause, resume, or abandon work, allowing for features like Suspense, concurrent rendering, and useTransition.
The use() hook reads the value of a resource (a Promise or context) directly during render. It integrates with Suspense boundaries to show fallbacks while the promise is pending.
import { use, Suspense } from 'react';
// Read a promise (works with Suspense)
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise); // suspends until resolved
return <h1>{user.name}</h1>;
}
// Read context (alternative to useContext)
function ThemedText() {
const theme = use(ThemeContext);
return <span style={{ color: theme.primary }}>Hello</span>;
}
// Usage with Suspense
function Page() {
const userPromise = fetchUser(1);
return (
<Suspense fallback={<Skeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}Server Actions (React 19) allow form submissions and mutations to call server-side functions directly without manual API routes. They integrate with useActionState, useFormStatus, and useOptimistic.
// server-actions.ts (server file)
'use server';
export async function createUser(formData: FormData) {
const name = formData.get('name') as string;
const email = formData.get('email') as string;
// Direct database access — no API route needed
const user = await db.user.create({
data: { name, email },
});
revalidatePath('/users');
return user;
}
// CreateUserForm.tsx (client component)
'use client';
import { useFormStatus } from 'react';
import { createUser } from './server-actions';
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? 'Creating...' : 'Create'}</button>;
}
function CreateUserForm() {
return (
<form action={createUser}>
<input name="name" placeholder="Name" />
<input name="email" placeholder="Email" />
<SubmitButton />
</form>
);
}Error boundaries are class components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI. They catch errors during rendering, in lifecycle methods, and in constructors of the whole tree below them.
// app/error.tsx (Next.js App Router)
'use client';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error(error);
// Send to error reporting: Sentry.captureException(error);
}, [error]);
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}The key prop helps React identify which items in a list changed, were added, or were removed. React uses keys to efficiently update the DOM during reconciliation.
| Key Source | Performance | Correctness | Recommendation |
|---|---|---|---|
| array index (i) | Bad — reshuffles on insert/delete | Broken state, focus | Avoid for dynamic lists |
| Math.random() | New key every render | Full re-mount | Never use |
| item.id (unique) | Stable, predictable | Correct reconciliation | Best practice ✅ |
| Composite key | Stable | Correct | Use when no id: ``${item.a}-${item.b}`` |