React hooks let you use state and lifecycle features inside function components. This reference covers every built-in hook with practical examples, common patterns, and the pitfalls that trip up most developers.
Quick reference
The 20 patterns that cover 95% of everyday React hook usage.
| Pattern | Hook | What it does |
|---|---|---|
const [n, setN] = useState(0) |
useState | Local state |
const [s, setS] = useState<T>(init) |
useState | Typed state |
setState(prev => prev + 1) |
useState | Functional update |
useEffect(() => {}, []) |
useEffect | Run once on mount |
useEffect(() => {}, [dep]) |
useEffect | Run when dep changes |
useEffect(() => () => cleanup()) |
useEffect | With cleanup |
const ref = useRef(null) |
useRef | DOM reference |
const count = useRef(0) |
useRef | Mutable value (no re-render) |
const val = useMemo(() => compute(), [dep]) |
useMemo | Memoize value |
const fn = useCallback(() => {}, [dep]) |
useCallback | Memoize function |
const ctx = useContext(MyCtx) |
useContext | Read context |
const [s, dispatch] = useReducer(fn, init) |
useReducer | Complex state logic |
useLayoutEffect(() => {}, []) |
useLayoutEffect | Before paint |
useId() |
useId | Unique ID |
useTransition() |
useTransition | Non-urgent updates |
useDeferredValue(val) |
useDeferredValue | Defer expensive re-renders |
useImperativeHandle(ref, () => ({})) |
useImperativeHandle | Expose ref methods |
useDebugValue(label) |
useDebugValue | DevTools label |
useSyncExternalStore(sub, snap) |
useSyncExternalStore | External store |
useInsertionEffect(() => {}) |
useInsertionEffect | CSS-in-JS injection |
useState — local state
The most-used hook. Stores a value and re-renders the component when it changes.
import { useState } from "react";
// Primitive
const [count, setCount] = useState(0);
// TypeScript typed
const [name, setName] = useState<string>("");
// Object state
const [user, setUser] = useState<{ name: string; age: number } | null>(null);
// Lazy initializer (runs once, useful for expensive defaults)
const [items, setItems] = useState<string[]>(() => {
return JSON.parse(localStorage.getItem("items") ?? "[]");
});
Functional updates — always use when new state depends on old:
// ✅ Correct — uses previous value
setCount(prev => prev + 1);
// ❌ Wrong inside async/batch — may use stale closure value
setCount(count + 1);
Object state — always spread, never mutate:
// ✅ Immutable update
setUser(prev => ({ ...prev!, age: 30 }));
// ❌ Mutation — React won't re-render
user.age = 30; // direct mutation
setUser(user); // same reference, no re-render
useEffect — side effects
Runs side effects (data fetching, subscriptions, DOM manipulation) after render.
import { useEffect, useState } from "react";
// Run once on mount (empty deps)
useEffect(() => {
document.title = "Hello";
}, []);
// Run when dep changes
useEffect(() => {
fetchUser(userId);
}, [userId]);
// Cleanup — returned function runs on unmount or before next run
useEffect(() => {
const id = setInterval(() => setTick(t => t + 1), 1000);
return () => clearInterval(id); // cleanup
}, []);
// Data fetching pattern with AbortController
useEffect(() => {
const controller = new AbortController();
async function load() {
try {
const res = await fetch(`/api/users/${id}`, { signal: controller.signal });
const data = await res.json();
setUser(data);
} catch (err) {
if ((err as Error).name !== "AbortError") setError(err as Error);
}
}
load();
return () => controller.abort();
}, [id]);
Dependency rules — include everything the effect reads from the component scope:
// ✅ Correct deps
useEffect(() => {
fetchData(userId, filter);
}, [userId, filter]);
// ❌ Missing deps — stale closure bug
useEffect(() => {
fetchData(userId, filter); // uses stale filter
}, [userId]); // eslint-plugin-react-hooks will warn
useRef — mutable refs
Two main uses: accessing DOM nodes, and storing mutable values without causing re-renders.
import { useRef, useEffect } from "react";
// DOM reference
function AutoFocus() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;
}
// Mutable value — does NOT trigger re-render on change
function Timer() {
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
function start() {
intervalRef.current = setInterval(() => console.log("tick"), 1000);
}
function stop() {
if (intervalRef.current) clearInterval(intervalRef.current);
}
return <button onClick={start}>Start</button>;
}
// Track previous value
function usePrevious<T>(value: T) {
const ref = useRef<T>(value);
useEffect(() => { ref.current = value; });
return ref.current; // previous render's value
}
useMemo — memoize values
Caches an expensive computed value; recomputes only when deps change.
import { useMemo } from "react";
const sortedList = useMemo(() => {
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
// Expensive filter
const filtered = useMemo(() =>
products.filter(p => p.price < maxPrice && p.category === cat),
[products, maxPrice, cat]
);
When to use: only when the computation is genuinely expensive (sorting/filtering large arrays, complex math). For cheap calculations, useMemo adds overhead without benefit.
useCallback — memoize functions
Returns a stable function reference. Useful when passing callbacks to memoized children to prevent unnecessary re-renders.
import { useCallback } from "react";
const handleSubmit = useCallback(async (data: FormData) => {
await api.post("/submit", data);
onSuccess?.();
}, [onSuccess]); // recreated only when onSuccess changes
// Pair with React.memo to avoid child re-renders
const Button = React.memo(({ onClick }: { onClick: () => void }) => (
<button onClick={onClick}>Click</button>
));
When to use: when a function is a dep of useEffect, or a prop to React.memo wrapped children. Don't wrap every function — it's premature optimisation.
useContext — read context
Reads the nearest context value above in the tree. Component re-renders when the context value changes.
import { createContext, useContext, useState } from "react";
// 1. Create context with a default
const ThemeCtx = createContext<"light" | "dark">("light");
// 2. Provide context high up in the tree
function App() {
const [theme, setTheme] = useState<"light" | "dark">("light");
return (
<ThemeCtx.Provider value={theme}>
<Page />
</ThemeCtx.Provider>
);
}
// 3. Consume anywhere below
function Page() {
const theme = useContext(ThemeCtx);
return <div className={theme}>Hello</div>;
}
// Custom hook pattern (recommended)
function useTheme() {
const ctx = useContext(ThemeCtx);
if (!ctx) throw new Error("useTheme must be inside ThemeCtx.Provider");
return ctx;
}
useReducer — complex state
Alternative to useState for complex state logic with multiple sub-values or when next state depends on previous.
import { useReducer } from "react";
type State = { count: number; status: "idle" | "loading" | "error" };
type Action =
| { type: "increment" }
| { type: "reset" }
| { type: "setLoading" }
| { type: "setError" };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "increment": return { ...state, count: state.count + 1 };
case "reset": return { count: 0, status: "idle" };
case "setLoading": return { ...state, status: "loading" };
case "setError": return { ...state, status: "error" };
default: return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0, status: "idle" });
return (
<div>
<p>Count: {state.count} ({state.status})</p>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "reset" })}>Reset</button>
</div>
);
}
useReducer vs useState: use useReducer when you have more than 2-3 related state fields, or when state transitions are complex and benefit from being explicit.
useLayoutEffect — before paint
Same as useEffect but fires synchronously after all DOM mutations, before the browser paints. Use for DOM measurements.
import { useLayoutEffect, useRef, useState } from "react";
function Tooltip() {
const ref = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState(0);
// Runs before paint — no flicker
useLayoutEffect(() => {
if (ref.current) setWidth(ref.current.offsetWidth);
}, []);
return <div ref={ref}>Width: {width}px</div>;
}
When to use: only when you need to measure DOM before it's shown. For everything else, use useEffect.
useId — stable unique IDs
Generates a unique ID that is stable across server and client renders (for SSR/hydration).
import { useId } from "react";
function Field({ label }: { label: string }) {
const id = useId();
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} />
</div>
);
}
Don't use for list keys (key prop). Use for ARIA associations and form labels.
useTransition — concurrent rendering
Marks state updates as non-urgent so React can interrupt them for higher-priority updates (user input stays responsive).
import { useState, useTransition } from "react";
function SearchPage() {
const [query, setQuery] = useState("");
const [results, setResults] = useState<string[]>([]);
const [isPending, startTransition] = useTransition();
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setQuery(e.target.value); // urgent — update input immediately
startTransition(() => {
// non-urgent — can be interrupted
setResults(filterLargeList(e.target.value));
});
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<ResultList items={results} />
</>
);
}
Custom hooks — reusable logic
Extract stateful logic into a function whose name starts with use. The function can call other hooks.
// useLocalStorage
function useLocalStorage<T>(key: string, initialValue: T) {
const [stored, setStored] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch {
return initialValue;
}
});
function setValue(value: T | ((prev: T) => T)) {
const next = value instanceof Function ? value(stored) : value;
setStored(next);
window.localStorage.setItem(key, JSON.stringify(next));
}
return [stored, setValue] as const;
}
// useWindowSize
function useWindowSize() {
const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight });
useEffect(() => {
const handler = () => setSize({ width: window.innerWidth, height: window.innerHeight });
window.addEventListener("resize", handler);
return () => window.removeEventListener("resize", handler);
}, []);
return size;
}
// useDebounce
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}
6 common mistakes
1. Missing useEffect dependencies
// ❌ Stale closure — filter never updates
useEffect(() => { fetchData(filter); }, []);
// ✅ Include all deps
useEffect(() => { fetchData(filter); }, [filter]);
2. Calling hooks conditionally
// ❌ Breaks rules of hooks — always call in the same order
if (condition) {
const [x, setX] = useState(0); // React will throw
}
// ✅ Condition inside the hook, not around it
const [x, setX] = useState(0);
if (condition) { /* use x */ }
3. Mutating state directly
// ❌ No re-render triggered
items.push(newItem);
setItems(items);
// ✅ New array
setItems(prev => [...prev, newItem]);
4. Forgetting cleanup in useEffect
// ❌ Memory leak — listener piles up on every render
useEffect(() => {
window.addEventListener("resize", handler);
});
// ✅ Return cleanup
useEffect(() => {
window.addEventListener("resize", handler);
return () => window.removeEventListener("resize", handler);
}, []);
5. useEffect for derived state
// ❌ Unnecessary effect + extra render
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${first} ${last}`);
}, [first, last]);
// ✅ Compute during render (no hook needed)
const fullName = `${first} ${last}`;
6. Overusing useMemo / useCallback
// ❌ Premature optimization — memoizing a cheap operation
const doubled = useMemo(() => count * 2, [count]);
// ✅ Just compute it
const doubled = count * 2;
FAQ
What are the rules of hooks?
Only call hooks at the top level (not inside loops, conditions, or nested functions). Only call hooks from React function components or custom hooks.
What's the difference between useEffect and useLayoutEffect?
useEffect runs asynchronously after the browser paints — no visual delay. useLayoutEffect runs synchronously before paint — use for DOM measurements to prevent flicker.
When should I use useReducer instead of useState?
When you have 3+ related pieces of state, complex transitions, or when the next state depends heavily on the previous one. useReducer makes state logic testable in isolation.
Why does my useEffect run twice in development?
React 18 Strict Mode intentionally mounts, unmounts, and remounts components in development to help you find missing cleanups. In production it runs once. Fix: add proper cleanup.
What's the difference between useRef and useState?
useState re-renders the component on change. useRef does not — it's a box whose .current you can mutate freely. Use refs for values that don't affect the UI (timer IDs, previous values, DOM nodes).
Can I use async functions directly in useEffect?
No — useEffect must return either nothing or a cleanup function. Instead, define an async function inside the effect and call it:
useEffect(() => {
async function load() {
const data = await fetchData();
setData(data);
}
load();
}, []);