React Hooks Quick Reference
Every core hook, what it is for, and the rules and gotchas that cause most React bugs.
Hooks let a function component remember values between renders and reach outside React. A component is a function React calls again on every render: useState and useRef keep values across those calls, useEffect synchronizes with things React does not control (network, timers, the DOM), and useMemo / useCallback cache work between renders.
Each render sees its own props and state, like a snapshot. Most hook bugs come from reading a snapshot from an older render (a stale closure) or from a dependency array that does not list everything an effect reads. Rows reflect React 19.
State & effects
- useState
const [n, setN] = useState(0)· functional update:setN(x => x+1)- useEffect
- Side effects after render. Return a cleanup fn. Deps array controls when it re-runs.
- Empty deps []
- Run once on mount (cleanup on unmount).
- Effect gotcha
- Missing a dependency → stale values. Include everything you read, or use a ref.
- useLayoutEffect
- Like useEffect but fires synchronously before paint — for DOM measurements.
- useState(() => init())
- lazy initial state: the function runs on the first render only, not every render
- setX(x + 1) twice
- adds 1, not 2 — both read this render’s
x;setX(p => p + 1)adds 2 - Objects in state
- replace, never mutate:
setUser({ ...user, name })— a mutated object does not re-render - Batching
- every setState in one event, timeout or promise commits as a single render (React 18+)
- StrictMode in dev
- effects run setup → cleanup → setup on mount, to surface missing cleanups
- useEffect(fn) with no deps
- runs after every render — rarely what you want
- Fetching in an effect
- abort or ignore the old request in cleanup, so a stale response cannot win the race
- Async effects
- the effect function cannot be async — declare an async function inside it and call it
- useEffectEvent(fn)
- read the latest props and state inside an effect without adding them as dependencies (19.2+)
Memoization
- useMemo
- Cache an expensive computed value:
useMemo(() => compute(a), [a]) - useCallback
- Cache a function identity:
useCallback(fn, [deps])— skips child re-renders only if the child ismemo - When to use
- Only when a value/fn is passed to memoized children or is genuinely expensive. Don’t over-memoize.
- memo(Component)
- skip re-rendering while props are shallowly equal — what makes stable callbacks pay off
- Object and array deps
- a fresh
{}or[]literal each render changes identity and re-fires the effect - useMemo is a hint
- React may throw the cache away; never rely on it to run code exactly once
- React Compiler
- memoizes components and values at build time, making most manual memoization unnecessary
Refs & context
- useRef
- Mutable box that persists across renders without causing re-renders:
ref.current - DOM ref
<input ref={inputRef} />theninputRef.current.focus()- useContext
const theme = useContext(ThemeContext)— read provided value- useReducer
const [state, dispatch] = useReducer(reducer, init)— complex state- ref.current = x
- never re-renders; avoid reading or writing refs during render — use them in handlers and effects
- ref as a prop
- React 19 function components take
reflike any prop — noforwardRefneeded - useImperativeHandle
- expose a small API such as
focus()through a ref instead of the whole DOM node - Context re-renders
- every consumer re-renders when the value changes — memoize the value or split the context
- use(SomeContext)
- reads context like
useContext, but may be called insideifand loops
Concurrency & utilities
- useTransition
[isPending, startTransition]— mark an update non-urgent so typing stays responsive- useDeferredValue(value)
- a lagging copy: render the slow list from it, the input from the live value
- startTransition(fn)
- the standalone form, for code outside components such as a router or store
- useId()
- unique id stable across server and client — for
htmlForandaria-*, not list keys - useSyncExternalStore
- subscribe to a store outside React (a global store, window size) without tearing
- useDebugValue(label)
- label a custom hook’s value in React DevTools
- useInsertionEffect
- fires before layout effects — meant for CSS-in-JS libraries injecting styles
Actions & forms (React 19)
- use(promise)
- suspend until a promise resolves; create it outside render (cached), never inside
- useActionState(fn, init)
- returns
[state, formAction, isPending]: run an action, keep its result - useOptimistic(state, fn)
- show the expected result while an action runs; it falls back to real state afterwards
- useFormStatus()
- from
react-dom:pendingof the enclosing form — call it in a child component - <form action={fn}>
- React calls
fnwith theFormDataand resets uncontrolled fields on success
Rules of hooks
- Top level only
- Never call hooks in loops, conditions, or nested functions — order must be stable.
- Components/hooks only
- Call from React function components or custom hooks (
useX). - Custom hooks
- Extract reusable logic into a
useSomething()that calls other hooks. - eslint-plugin-react-hooks
rules-of-hooksandexhaustive-depscatch most of the bugs on this page- key={id} to reset
- a different
keyremounts a component with fresh state — no reset effect needed - Derive, don’t sync
- compute values from props and state during render instead of copying them via an effect
Recipes
Fetch data in an effect without race conditions
function Profile({ userId }) {
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch('/api/users/' + userId, { signal: controller.signal })
.then((res) => res.json())
.then(setUser)
.catch((err) => {
if (err.name !== 'AbortError') setError(err);
});
return () => controller.abort(); // a newer userId cancels the old request
}, [userId]);
if (error) return <p>Failed to load.</p>;
if (!user) return <p>Loading…</p>;
return <h1>{user.name}</h1>;
}Without the cleanup, a slow response for an old userId can arrive last and overwrite the new one. In development, StrictMode runs the effect twice, so one aborted request in the network tab is expected.
A useDebounce custom hook
function useDebounce(value, delay = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id); // each change restarts the timer
}, [value, delay]);
return debounced;
}
// usage: search only once typing pauses
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 400);
useEffect(() => {
if (debouncedQuery) search(debouncedQuery);
}, [debouncedQuery]);The cleanup is what makes it a debounce: every change cancels the pending timeout. The input stays responsive because it renders from query, not from the debounced value.
Fix a stale closure in setInterval
// Bug: the callback captured count from the first render, so it sticks at 1
useEffect(() => {
const id = setInterval(() => setCount(count + 1), 1000);
return () => clearInterval(id);
}, []);
// Fix: a functional update always receives the latest state
useEffect(() => {
const id = setInterval(() => setCount((c) => c + 1), 1000);
return () => clearInterval(id);
}, []);With an empty dependency array the effect, and the count it closed over, both come from the first render. The exhaustive-deps lint rule flags the first version.
An optimistic like button (React 19)
function LikeButton({ likes, onLike }) {
const [optimisticLikes, addOptimistic] = useOptimistic(
likes,
(current, delta) => current + delta
);
const [isPending, startTransition] = useTransition();
function handleClick() {
startTransition(async () => {
addOptimistic(1); // the UI shows +1 immediately
await onLike(); // server call; the parent then updates likes
});
}
return <button onClick={handleClick} disabled={isPending}>♥ {optimisticLikes}</button>;
}When the transition ends, React drops the optimistic value and shows the real likes, so a failed request rolls back by itself. Optimistic updates must happen inside a transition or form action.
Reset a form when the selected item changes
// Instead of an effect that clears the fields when contactId changes...
<EditContact key={contactId} contact={contact} />
// ...a new key unmounts the old form and mounts a fresh one with fresh state.Resetting state in an effect renders once with the stale values and then again. Changing the key resets every piece of state in that subtree in a single render.
Questions about React Hooks Cheat Sheet
What is the difference between useEffect and useLayoutEffect?
Both run after React updates the DOM, but at different moments. useEffect runs after the browser has painted, so it never blocks the screen from updating; use it for data fetching, subscriptions, timers and logging. useLayoutEffect runs synchronously after the DOM changes but before the browser paints, so anything you measure or change there is visible in the very first frame. Use it only when you must read layout, such as an element size for positioning a tooltip, and would otherwise see a flicker. Because it blocks painting, overusing it makes an app feel slower.
Why does my useEffect run twice?
In development, React StrictMode deliberately mounts every component, unmounts it and mounts it again, so each effect runs its setup, then its cleanup, then its setup a second time. This happens only in development, never in a production build, and it exists to expose effects that do not clean up after themselves, such as subscriptions that are never removed or requests that are never cancelled. The fix is to write a proper cleanup function, not to remove StrictMode. If a second run causes a real problem, the same bug would appear in production whenever the component remounts.
When should I use useMemo and useCallback?
Use them when a cached value or function identity actually saves work. useMemo helps when a calculation is noticeably slow, or when the result is passed to a component wrapped in memo or used as an effect dependency. useCallback does the same for functions: it only pays off when the function goes to a memoized child or into a dependency array. Wrapping everything adds code and comparison cost for no gain. Measure first with the React DevTools profiler. Projects using the React Compiler get this memoization automatically and rarely need either hook by hand.
What is the difference between useState and useRef?
Both keep a value alive between renders, but only state is tied to rendering. Calling a state setter schedules a re-render, and each render reads the value as a fixed snapshot. Changing ref.current updates the value immediately and never triggers a render, so the screen does not change. Use state for anything the UI displays. Use a ref for values the UI does not depend on, such as a timer id, the previous value of a prop, or a DOM element. Avoid reading refs during rendering, since the output would not update when they change.