Cheatsheet
Every React hook: what it does and when to use it
All twenty built in hooks through React 19.2, each with its exact signature, the situation that calls for it, and the gotcha that costs people an afternoon. Grouped by job: state, effects, performance, refs, context, and the newer form and action hooks.
State hooks
| Hook and signature | Reach for it when | Gotcha |
|---|---|---|
| useState const [s, setS] = useState(initial) | A component owns one independent piece of local state. | Updates are queued, not immediate. Reading s right after setS gives the old value. Use the updater form when the next value depends on the last. |
| useState useState(() => expensive()) | The initial value costs real work to compute. | Passing expensive() directly runs it on every render and throws the result away. The function form runs once. |
| useReducer const [s, dispatch] = useReducer(reducer, initialArg, init?) | Several state values change together, or the next state depends on the last in non trivial ways. | The reducer must be pure. Side effects inside it run twice in Strict Mode development and will look like a bug. |
| useSyncExternalStore useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?) | You are reading from something outside React: a store library, localStorage, a media query, the browser online flag. | getSnapshot must return a cached reference. Building a fresh object each call causes an infinite render loop. |
| useOptimistic const [optimistic, add] = useOptimistic(state, updateFn) | You want the UI to show the result of a pending action before the server confirms it. | The optimistic value reverts automatically when the action settles. If the real state never updates, the UI snaps back and looks broken. |
Gotcha for the whole group: React bails out of a re-render when the new state is Object.is equal to the old one. Mutating an array or object in place and calling the setter with the same reference renders nothing at all.
Effect hooks
| Hook and signature | Reach for it when | Gotcha |
|---|---|---|
| useEffect useEffect(setup, deps?) | You need to synchronize with something outside React: a subscription, a timer, a non React widget, an analytics call. | Strict Mode runs setup, cleanup, then setup again in development. If that breaks your effect, the cleanup is wrong. |
| useEffect useEffect(() => { ...; return cleanup }, []) | Setup should run once on mount and tear down on unmount. | An empty dependency array with a stale closure inside is the single most common React bug. The linter is right; do not silence it. |
| useLayoutEffect useLayoutEffect(setup, deps?) | You must measure the DOM and adjust it before the browser paints, such as positioning a tooltip. | It blocks paint, so it is a performance hazard. It also does not run during server rendering and will warn about it. |
| useInsertionEffect useInsertionEffect(setup, deps?) | You are writing a CSS in JS library and must inject style tags before any layout read. | Refs are not attached and state updates are not allowed yet. Application code should never need it. |
| useEffectEvent const onX = useEffectEvent(fn) | An effect must read a prop or state without re-running when it changes. Stable since React 19.2. | Call it only from inside an effect, never during render, and never pass it to another component. |
Effects you should delete rather than fix
| Anti pattern | Do this instead |
|---|---|
| useEffect to derive state from props | Compute it during render. If it is expensive, wrap that computation in useMemo. |
| useEffect to reset state when a prop changes | Give the component a key. React remounts it and the state resets for free. |
| useEffect to respond to a click | Put the logic in the event handler. Effects are for synchronization, not for events. |
| useEffect to fetch on mount | Use the framework loader, a server component, or a query library that handles races and caching. |
| useEffect to notify the parent of a change | Call the parent's callback in the same handler that made the change. |
| useEffect to keep two states in sync | Keep one state and derive the other, or lift the shared value up one level. |
Gotcha: when an effect does need to fetch, guard against races. Set an ignore flag in the cleanup, or pass an AbortController signal, or a slow first response will overwrite a fast second one.
Performance hooks
| Hook and signature | Reach for it when | Gotcha |
|---|---|---|
| useMemo const v = useMemo(() => calc(a, b), [a, b]) | A computation is genuinely expensive, or the identity of an object must stay stable for a downstream dependency array. | Memoising is not free. Measure first; a cheap calculation wrapped in useMemo is slower than the calculation. |
| useCallback const fn = useCallback(() => {}, [deps]) | The function is passed to a memoised child or listed in another hook's dependency array. | Pointless unless the receiver actually compares it. Wrapping every handler is cargo cult and costs memory. |
| useTransition const [isPending, startTransition] = useTransition() | An update is slow and should not block typing or clicking. Wrap the slow update, show isPending. | The function you pass must update state synchronously. State set after an await inside it is not part of the transition. |
| useDeferredValue const deferred = useDeferredValue(value, initialValue?) | You do not control the update but want an expensive subtree to lag behind a fast input. | It only helps if the expensive child is memoised. Otherwise the whole tree re-renders anyway. |
Gotcha: the React Compiler auto memoises most of what useMemo and useCallback are used for. If your build enables it, delete the manual wrappers rather than stacking them.
Ref and context hooks
| Hook and signature | Reach for it when | Gotcha |
|---|---|---|
| useRef const ref = useRef(initialValue) | You need a mutable box that survives renders without causing one: a DOM node, a timer id, a previous value. | Changing ref.current never re-renders. Reading or writing it during render breaks concurrent rendering. |
| useImperativeHandle useImperativeHandle(ref, createHandle, deps?) | A parent must call a method on your component, such as focus or scrollIntoView, and you want to expose only that. | In React 19 ref is a normal prop, so forwardRef is no longer needed to receive it. |
| useContext const value = useContext(ThemeContext) | A value must reach a deep child without threading it through every layer of props. | Every consumer re-renders whenever the provider value changes identity. Memoise the value object. |
| use const value = use(promiseOrContext) | You want to read a context or unwrap a promise, including inside a condition or a loop. React 19. | The promise must be created outside render, by a framework or a cache. Creating one in render suspends forever. |
| useId const id = useId() | You need a unique, hydration safe id to wire a label to an input or an aria attribute to its target. | Never use it as a list key. It identifies a component instance, not a data item. |
| useDebugValue useDebugValue(value, format?) | You are shipping a custom hook and want React DevTools to show something meaningful. | Only useful inside a custom hook, and only visible in DevTools. It does nothing in production. |
Form and action hooks
| Hook and signature | Reach for it when | Gotcha |
|---|---|---|
| useActionState const [state, formAction, isPending] = useActionState(action, initialState, permalink?) | A form submits to an async action and you want the result, the pending flag, and progressive enhancement. | The action receives previousState as its first argument and the FormData as its second. Getting that order wrong is the usual bug. |
| useFormStatus const { pending, data, method, action } = useFormStatus() | A submit button or spinner needs to know the form is in flight without prop drilling. | Imported from react-dom, not react, and it only reports on a parent form. Calling it in the component that renders the form returns pending false forever. |
import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus(); // must live INSIDE the form
return <button disabled={pending}>{pending ? 'Saving...' : 'Save'}</button>;
}
export function ProfileForm() {
const [state, formAction] = useActionState(
async (prev, formData) => { // prev first, formData second
const res = await save(formData);
return res.ok ? { message: 'Saved' } : { error: res.error };
},
{ message: '' }
);
return (
<form action={formAction}>
<input name="displayName" />
<SubmitButton />
{state.error ? <p role="alert">{state.error}</p> : null}
</form>
);
} Rules and dependency arrays
| Rule | Why |
|---|---|
| Call hooks at the top level only | React matches hooks to state by call order. A hook inside an if or a loop shifts every later hook. |
| Call hooks from components or custom hooks | Plain functions and event handlers have no component to attach state to. |
| use() is the one exception | It may be called conditionally, which is the whole reason it was added. |
| deps omitted | Runs after every single render. Almost always a mistake outside a deliberate logger. |
| deps is [] | Runs once after mount and cleans up on unmount. Everything it closes over is frozen at that moment. |
| deps is [a, b] | Re-runs when a or b changes by Object.is comparison. Objects and arrays fail that test every render. |
| Setter functions are stable | setState, dispatch, and refs never change identity, so they never need to be listed. |
| Never disable exhaustive-deps | The lint suppression hides a stale closure. Move the value into a ref or an effect event instead. |
| Custom hooks start with use | The naming convention is what lets the linter and the compiler apply hook rules to your function. |
| Strict Mode double invokes | Render functions, reducers, and effect setup and cleanup all run twice in development to surface impurity. |
Gotcha: a dependency array is a correctness declaration, not a performance knob. When the linter wants a dependency you do not want to react to, that is the signal to reach for useEffectEvent or a ref, not to trim the array.
Custom hooks worth writing once
A custom hook is just a function that calls other hooks. These are the ones almost every codebase ends up needing, with the primitive each is built on.
| Hook | Built on | Note |
|---|---|---|
| usePrevious(value) | useRef plus useEffect | Returns the value from the previous render. Undefined on the first one. |
| useDebounce(value, ms) | useState plus useEffect | The cleanup must clear the timer, or every keystroke leaves one running. |
| useMediaQuery(query) | useSyncExternalStore | Give it a server snapshot or hydration will mismatch on the first paint. |
| useLocalStorage(key, initial) | useSyncExternalStore | Subscribe to the storage event so two tabs stay in agreement. |
| useOnClickOutside(ref, fn) | useEffect plus useEffectEvent | Listen on pointerdown, not click, or the same click that opened it closes it. |
| useInterval(fn, ms) | useEffect plus useEffectEvent | Keep the callback out of the dependency array or the timer restarts every render. |
| useEventListener(target, type, fn) | useEffect | Always return the removeEventListener call. Strict Mode will catch a missing one. |
| useCopyToClipboard() | useState plus a handler | The clipboard API needs a secure context and a real user gesture. |
| useIsomorphicLayoutEffect | useLayoutEffect or useEffect | Picks useEffect on the server to silence the layout effect warning during SSR. |
| useToggle(initial) | useReducer | A one line reducer beats useState plus an inline arrow in every consumer. |
Gotcha: a custom hook shares logic, never state. Two components calling the same hook get two completely independent copies of everything inside it. If they need to share a value, it belongs in context or a store.
Keep going
Typing hook state and custom hook returns is covered in the TypeScript cheatsheet, and the classes on the markup around them are in the Tailwind CSS v4 cheatsheet.
Weighing the framework itself? Read React vs Svelte, or browse the frontend tool directory. Everything else sits in the cheatsheet index.