Every built-in hook in React 19.2 (and the use API), each with one example and the detail people forget.
Rules of Hooks
- Call hooks at the top level of a component or custom hook: never in conditions, loops, nested functions or after an early
return. - React matches hooks to their state by call order, which is why the order must be identical on every render.
- A custom hook is a
use-prefixed function that calls other hooks; each call gets its own state. useis the exception: it may be called insideifand loops, but still only in components and hooks.- Enable
eslint-plugin-react-hooks:rules-of-hooksandexhaustive-depscatch most bugs.
State
useState:const [count, setCount] = useState(0). Use the updatersetCount((c) => c + 1)when the next value depends on the previous one.useState(() => init())runsinitonly once.useReducer: the update logic lives in a pure function. A third argument,init, computes the initial state lazily.
function reducer(todos, action) {
switch (action.type) {
case 'added': return [...todos, action.todo];
case 'removed': return todos.filter((t) => t.id !== action.id);
default: throw new Error(`Unknown action: ${action.type}`);
}
}
const [todos, dispatch] = useReducer(reducer, []);
dispatch({ type: 'added', todo: { id: 1, text: 'Ship it' } });Context & refs
useContext:const theme = useContext(ThemeContext)reads the nearest provider above. Consumers re-render when the value changes (Object.is). React 19 lets you render<ThemeContext value="dark">instead of<ThemeContext.Provider>.useRef:const inputRef = useRef(null), then<input ref={inputRef} />andinputRef.current.focus(). Changing.currentnever re-renders; don’t read or write it during render.useImperativeHandle: exposes a custom handle instead of the DOM node. In React 19,refis a regular prop, soforwardRefis no longer needed.
function Field({ ref }) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
clear: () => { inputRef.current.value = ''; },
}), []);
return <input ref={inputRef} />;
}Effects
useEffect: syncs with an external system. The cleanup runs before the next setup and on unmount.
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // no array: every render; []: after mount onlyuseLayoutEffect: same signature, but runs after DOM changes and before paint, so you can measure without flicker:useLayoutEffect(() => setHeight(ref.current.offsetHeight), []). It blocks painting; preferuseEffect.useInsertionEffect: for CSS-in-JS libraries, fires before any layout effect:useInsertionEffect(() => injectStyle(rule), [rule]). It cannot update state, and refs are not attached yet.useEffectEvent(stable since 19.2): reads the latest props and state from inside an effect without making them dependencies. Call it only from effects; never pass it on or list it in deps.
const onConnected = useEffectEvent(() => showToast('Connected', theme));
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', onConnected);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // changing theme does not reconnect| Hook | Runs |
|---|---|
useInsertionEffect |
Before layout effects |
useLayoutEffect |
After DOM updates, before paint |
useEffect |
Usually after paint |
In Strict Mode (development only), React runs setup, cleanup, then setup again on mount to expose missing cleanups.
Performance
useMemo:const visible = useMemo(() => filterTodos(todos, tab), [todos, tab])caches a calculation between renders. Treat it as an optimization, not a guarantee.useCallback:const onSubmit = useCallback((data) => post(id, data), [id])caches a function; it equalsuseMemo(() => fn, deps).- Both only help when the value goes to a
memochild or into a dependency array. The React Compiler can add this memoization for you.
Transitions & deferred values
useTransition:const [isPending, startTransition] = useTransition(), thenstartTransition(() => setTab('posts')). Marks an update as non-urgent. Since React 19 the function may be async (an Action). Not for controlled text inputs.useDeferredValue:const deferred = useDeferredValue(query); render the slow list withdeferred, andquery !== deferredmeans stale results are showing. React 19 adds an optionalinitialValue. Pass primitives or objects created outside render.
Actions & forms
useActionState: returns[state, formAction, isPending]. The action receives(previousState, payload); passformActionto a form’sactionprop, or call it insidestartTransition.
async function addToCart(prevState, formData) {
const res = await api.add(formData.get('id'));
return res.ok ? { error: null } : { error: 'Could not add item' };
}
const [state, formAction, isPending] = useActionState(addToCart, { error: null });
// <form action={formAction}> ... {state.error}useOptimistic: shows a temporary value while an Action is pending, then falls back to the real one. Call the setter inside an Action.
const [optimisticLikes, setOptimisticLikes] = useOptimistic(likes);
const like = () => startTransition(async () => {
setOptimisticLikes(likes + 1); // visible immediately
setLikes(await api.like()); // the real value when the Action ends
});useFormStatus(fromreact-dom):const { pending } = useFormStatus()inside aSubmitButtongives the parent form’s status. It must be rendered inside the form, not in the component that renders it.
Utilities & use
useId:const id = useId(), then<label htmlFor={id}>and<input id={id} />. Stable across server and client rendering; not for list keys.useSyncExternalStore: subscribes to a store outside React. Declaresubscribeoutside the component, and havegetSnapshotreturn the same value while nothing changed, or React re-renders forever.
function subscribe(onChange) {
window.addEventListener('online', onChange);
window.addEventListener('offline', onChange);
return () => {
window.removeEventListener('online', onChange);
window.removeEventListener('offline', onChange);
};
}
const isOnline = useSyncExternalStore(subscribe, () => navigator.onLine, () => true);useDebugValue:useDebugValue(isOnline ? 'Online' : 'Offline')inside a custom hook labels it in React DevTools.use:const comments = use(commentsPromise)suspends until the promise resolves (show aSuspensefallback; errors reach an Error Boundary, nottry/catch). Pass a cached or server-created promise; one created during render is new on every render.use(ThemeContext)works likeuseContext, conditionally.
Gotchas
- Stale closures: callbacks and effects see the values of the render that created them. Fix the deps, use the updater form or
useEffectEvent. - State is a snapshot: calling
setCount(count + 1)twice in one handler adds 1;setCount((c) => c + 1)twice adds 2. - Unstable dependencies: an object or function created during render is new every time, so the effect re-runs on every render. Move it inside the effect or memoize it.
- Derive, don’t sync: if a value can be computed from props or state during render, don’t copy it into state with an effect.
- Keys reset state: changing a component’s
keyremounts it with fresh state.