React Hooks Cheatsheet — All 17 Built-In Hooks Plus Custom Patterns, with Real Examples and Pitfalls
React hooks cheat sheet — all 17 built-in hooks (useState / useEffect / useMemo / useTransition / useFormStatus...) with real examples and pitfalls.
Runs locally
CategoryDeveloper & DevOps
Best forFormatting, validating, shrinking, or inspecting code-adjacent text.
28 hooks
Built-in (18)
useState
Built-in
Signatureconst [state, setState] = useState<T>(initial: T | (() => T))
The bread-and-butter state hook. Returns a tuple of [current value, setter]. The setter is stable across renders; calling it triggers a re-render with the new value.
✓ When to use
Any per-component value that needs to trigger a re-render when it changes — form fields, toggles, counters, fetched data, UI mode.
✗ When to avoid
Derived values (compute them inline or with useMemo instead). Refs that should NOT trigger re-render (use useRef). Complex state with many transitions (use useReducer).
⚠ Common pitfall: Stale closure: if you call setState(count + 1) twice in a row, only one increment happens because both reads of `count` capture the same value. Always use the functional form setState(c => c + 1) when the next value depends on the previous.
Examples
const [count, setCount] = useState(0);
// functional form is mandatory when next depends on prev
const inc = () => setCount(c => c + 1);
// lazy initializer — fn runs once, not on every render
const [grid, setGrid] = useState(() => buildExpensiveGrid(1000));
State hook for state shapes with multiple transitions. The reducer is a (state, action) => state function. dispatch is stable, so it is safe to pass to children without breaking memoization.
✓ When to use
Form state with 5+ fields. Async flows with idle/loading/success/error transitions. Anywhere you find yourself writing several related setState calls in one handler.
✗ When to avoid
Single boolean or counter (useState is shorter). State that lives in only one component and never transitions through more than two values (useState wins on line count).
⚠ Common pitfall: The reducer must be pure — no fetch, no setTimeout, no DOM access inside. Side effects belong in useEffect or the handler that calls dispatch.
Examples
type Action = { type: 'inc' } | { type: 'reset' };
function reducer(state: number, action: Action) {
switch (action.type) {
case 'inc': return state + 1;
case 'reset': return 0;
}
}
const [count, dispatch] = useReducer(reducer, 0);
// async flow modeled as a state machine
type S = { status: 'idle' | 'loading' | 'ok' | 'err'; data?: User };
type A = { type: 'start' } | { type: 'ok'; data: User } | { type: 'err' };
Run side effects AFTER the browser paints. The optional cleanup function runs before the next effect and on unmount. Deps array controls when the effect re-fires.
✓ When to use
Subscribing to external systems (WebSocket, event listener, observer). Sync with non-React state (localStorage, document.title). Anything that should react to a prop / state change AFTER the DOM updates.
✗ When to avoid
Deriving state from props (just compute it inline). Running on every render with no cleanup (you do not need useEffect for that — it is just code in the body). Resetting state when a prop changes (use `key` prop or compute during render with a previous-value check).
⚠ Common pitfall: Missing deps = stale closure: the effect captures the values from the render where it was DEFINED. Lint with `eslint-plugin-react-hooks` exhaustive-deps and either include every value you read, or move the value into a ref or out of the component.
Examples
useEffect(() => {
const id = setInterval(() => setTick(t => t + 1), 1000);
return () => clearInterval(id); // cleanup is mandatory for timers
}, []);
Same signature as useEffect but fires SYNCHRONOUSLY after the DOM mutation and BEFORE the browser paints. Use to measure or mutate layout without a flash.
✓ When to use
Measuring DOM size and using it to position something in the same paint. Reading scroll position. Synchronously fixing layout before the user sees the flash of the wrong state.
✗ When to avoid
Anything that does not need to be visually consistent on the same paint. Heavy work (you block paint and tank perf). Server rendering — useLayoutEffect does not run on the server and React warns.
⚠ Common pitfall: It blocks paint. Long work here = jank. If you see "useLayoutEffect does nothing on the server" warnings during SSR, switch to useEffect or guard the import.
Examples
useLayoutEffect(() => {
const { height } = ref.current!.getBoundingClientRect();
setRowHeight(height); // setState is safe — it batches before paint
}, []);
Niche hook designed for CSS-in-JS libraries. Fires synchronously BEFORE any DOM mutations so libraries can inject <style> tags without causing layout to thrash.
✓ When to use
Almost never in app code. The intended consumers are styled-components / Emotion / Stitches authors who need to insert generated stylesheets before React commits.
✗ When to avoid
You almost certainly want useEffect or useLayoutEffect. Reading layout here gives wrong values — the DOM has not been mutated yet. No refs are attached when this fires.
⚠ Common pitfall: Refs are not yet attached and you cannot use setState here — those are documented React contract. Treat this as a library-only hook.
Examples
// styled-components-style use case
useInsertionEffect(() => {
const tag = document.createElement('style');
tag.textContent = generatedCss;
document.head.appendChild(tag);
return () => tag.remove();
}, [generatedCss]);
useMemo
Built-in
Signatureconst value = useMemo<T>(() => compute(), deps)
Cache the return value of an expensive computation between renders. The function runs only when one of the deps changes.
✓ When to use
Expensive pure computation (sort + filter of a long list, heavy parsing). Stabilizing a referenced object/array passed to memoized children or used as another hook deps.
✗ When to avoid
Cheap computations — useMemo itself has overhead. Anywhere you would only "feel safer" with it. Wrapping every value is a known anti-pattern that bloats the bundle and slows things down.
⚠ Common pitfall: useMemo is a hint, not a guarantee — React may discard the cached value to free memory. Never put logic that MUST run only once inside; use useEffect with [] or a ref instead.
Memoize a function reference between renders so that React.memo children or downstream hooks see a stable callback. Equivalent to useMemo(() => fn, deps).
✓ When to use
Passing a handler to a memoized child (React.memo / PureComponent). Including a function in another hook deps array. Otherwise: skip it.
✗ When to avoid
Non-memoized children do not benefit (they re-render anyway). Wrapping every handler is the same anti-pattern as wrapping every value in useMemo.
⚠ Common pitfall: Same closure trap as useEffect: missing a dep means the cached function reads STALE values. exhaustive-deps lint is mandatory.
Signatureconst ref = useRef<T>(initial: T): { current: T }
Returns a mutable .current object that survives re-renders WITHOUT triggering them. Two uses: (1) DOM node reference via the ref attribute, (2) mutable instance value (like a class field).
✓ When to use
Read or focus a DOM node. Store a timer id, previous value, or instance flag that must not cause a re-render. Hold a mutable cache.
✗ When to avoid
Anything the UI must react to — that needs useState. Refs are skipped during reconciliation, so mutating .current never re-renders.
⚠ Common pitfall: Reading ref.current during render is fragile — for DOM refs the value is null on first render. Read in useEffect / useLayoutEffect or event handlers, not the render body.
// instance value pattern — no re-render on change
const lastSeenRef = useRef(0);
function onScroll() { lastSeenRef.current = window.scrollY; }
useImperativeHandle
Built-in
SignatureuseImperativeHandle<T, R extends T>(ref, () => R, deps?)
Customize the value exposed to a parent via ref. Pair with forwardRef. Use to expose a narrow imperative API like { focus, scrollIntoView } instead of the raw DOM node.
✓ When to use
A reusable input component that exposes focus() and clear() to parents. A modal that exposes open()/close(). Any wrapped component where the parent needs a small imperative API.
✗ When to avoid
Anything that could be expressed declaratively via props — prefer that. Exposing the whole DOM node — just forward the ref directly.
⚠ Common pitfall: Without forwardRef, ref does nothing on a function component. Forgetting the deps array means the imperative API is re-created every render.
Read the value of a Context inside a function component. The component re-renders whenever the nearest Provider above it pushes a new value.
✓ When to use
Cross-cutting concerns: theme, current user, i18n, feature flags. Any value many components in a subtree need without prop-drilling.
✗ When to avoid
Frequently-changing values (every consumer re-renders). Replacement for a real state library when you need fine-grained updates — use Zustand / Jotai / Redux selectors instead.
⚠ Common pitfall: Putting an inline object as Provider value re-creates it every render and re-renders every consumer. Memoize the value with useMemo: <Ctx.Provider value={useMemo(() => ({a, b}), [a, b])}>.
Mark a state update as non-urgent. React keeps the previous UI interactive while the transition renders in the background, and isPending tells you it is running.
✓ When to use
Filtering a long list while typing. Tab switches where the new tab is expensive to render. Anywhere a slow render makes typing or clicking feel sluggish.
✗ When to avoid
Urgent updates that the user expects to be immediate (input value, button toggle). Side effects — startTransition is for state updates, not for fetch/timer.
⚠ Common pitfall: Wrapping a fetch call does nothing — only the state UPDATES inside the callback are deferred. Use Suspense + use() for data, useTransition for the resulting state.
Return a deferred copy of a value. The deferred copy lags behind the real one during expensive renders, letting urgent updates win. Like useTransition but for values you do not own (props).
✓ When to use
You receive a frequently-changing prop and want to render a heavy view from it without blocking typing. Pair with React.memo on the heavy child so the deferred prop only triggers the slow render.
✗ When to avoid
You own the setter — useTransition is more explicit. Tiny values where deferring causes flicker without performance benefit.
⚠ Common pitfall: Without React.memo on the heavy consumer, useDeferredValue still triggers the slow render — just slightly later. The memo is what makes it work.
Subscribe to an external (non-React) data source — Redux store, browser API, observable — without tearing under concurrent rendering. Returns the current snapshot and re-renders when the store notifies.
✓ When to use
Writing a custom hook over a non-React store (window.matchMedia, document.visibilityState, a third-party state manager). Library authors making concurrent-safe bindings.
✗ When to avoid
Plain component state (useState) or a context-based store (useContext). Most app code never needs this directly — it is plumbing.
⚠ Common pitfall: getSnapshot MUST return a referentially stable value when nothing changed — returning a fresh object every call causes an infinite render loop. Cache snapshots in a closure.
Generate a unique, deterministic id that is stable across server and client renders. Designed for accessibility attributes that need to associate a label with an input.
✓ When to use
aria-labelledby, htmlFor + id pairs in a reusable form component, aria-describedby on tooltips. Anywhere you need a unique id that survives SSR.
✗ When to avoid
Keys in a list (use the data id). CSS selectors or DOM lookup ids — the returned string contains : and is not a valid CSS identifier without escaping.
⚠ Common pitfall: The id contains colons (e.g. `:r1:`) — fine in HTML attributes but breaks naive querySelector(`#${id}`) calls. Use document.getElementById, which accepts any string.
Examples
function Field({ label }: { label: string }) {
const id = useId();
return <><label htmlFor={id}>{label}</label><input id={id} /></>;
}
React 19+. Show an immediate, optimistic UI while a server action is in flight. Returns a temporary state computed from the real state plus pending optimistic updates; reconciles when the action resolves.
✓ When to use
Like / upvote button. Sending a chat message. Toggling a setting where the server confirmation is fast but you want zero latency on the UI.
✗ When to avoid
Long server roundtrips where failure is common — the rollback flash is worse than just showing a spinner. Cases where the optimistic value would be wrong (server normalizes input).
⚠ Common pitfall: Must be called inside a transition or a server action — calling it from a normal event handler does nothing visible. The optimistic value disappears as soon as the action resolves; sync to local state if you need to keep it.
Examples
const [optimisticLikes, addLike] = useOptimistic(
likes,
(current, _: number) => current + 1,
);
async function onLike() {
addLike(1); // UI updates immediately
await serverLike(postId); // real call
}
React 19+. Manage state that transitions via an async action — typically a form submit. Combines a reducer-style update, automatic pending flag, and progressive enhancement with form actions.
✓ When to use
Form submission with server validation (e.g. login form returning {error: "wrong password"}). Any async transition where you want pending state without writing useState + try/finally by hand.
✗ When to avoid
Synchronous state changes (useState / useReducer is simpler). Non-form-like flows — useTransition gives more flexibility.
⚠ Common pitfall: The reducer signature is (state, formData) => Promise<state>, NOT the classic (state, action). isPending is true even if the user navigates away mid-action — clean up in an effect if it matters.
React 19+. Read the submission status of the PARENT <form>. Designed to be called from a button/input INSIDE a form so it can disable itself or show a spinner without prop-drilling.
✓ When to use
A reusable <SubmitButton> that disables itself while the form is submitting. Field-level inline progress indicators that do not need to know the form's implementation.
✗ When to avoid
Outside of a form (returns { pending: false, data: null }). When you already have isPending from useActionState — just thread that through.
⚠ Common pitfall: Returns false / null if the component is NOT a descendant of the form. Calling it on the same component as the <form> always returns false — it reads the PARENT form, not the current one.
React 19+. The unified read primitive — pass a Promise to suspend until it resolves, or a Context to read its value. Unlike other hooks, use() can be called inside if/loop/early-return.
✓ When to use
Reading a server-action Promise in a Suspense boundary (the canonical way to render async data in React 19). Conditional context reads (useContext cannot be called conditionally).
✗ When to avoid
Creating a fresh Promise inside the component body — every render creates a new Promise and you suspend forever. Pass the Promise in from a parent or memoize it.
⚠ Common pitfall: use(somePromise) reads CACHED promises. If you do not stabilize the Promise (in a parent, via cache(), or React Query), you cause an infinite suspend-resolve-render loop.
Examples
function User({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise); // suspends until resolved
return <div>{user.name}</div>;
}
// conditional context read — useContext cannot do this
const value = condition ? use(CtxA) : use(CtxB);
Three-element tuple for boolean state: current value, a stable toggle function, and the raw setter. The toggle ignores arguments so it can be passed directly as onClick.
✓ When to use
Disclosure / accordion open state. Modal open/close. Light/dark toggle. Anywhere you write `setOpen(o => !o)` more than once.
✗ When to avoid
Non-boolean state. Booleans that need optimistic / async transitions — use useOptimistic or useActionState.
⚠ Common pitfall: If you pass toggle to onClick and TypeScript complains about MouseEvent vs void — overload the signature so it ignores its argument: () => void.
Same shape as useState but persists to window.localStorage and rehydrates on mount. Should serialize via JSON and guard against SSR (window undefined).
✓ When to use
User preferences (theme, sidebar collapsed). Form drafts. Anything that should survive a page reload but does not belong on the server.
✗ When to avoid
Sensitive data (everyone can read localStorage in DevTools). Large blobs (5 MB cap per origin and synchronous I/O). Cross-tab sync without listening to the `storage` event.
⚠ Common pitfall: JSON.parse throws on bad data — wrap in try/catch and fall back to initial. typeof window === "undefined" check is mandatory in Next.js / Astro SSR or the build crashes.
Returns a value that lags behind the input by delayMs of "quiet time" — every change resets the timer. Use to throttle expensive work driven by frequently-changing values.
✓ When to use
Search-as-you-type before firing an API call. Auto-save drafts. Window resize → recompute layout.
✗ When to avoid
Each individual update matters (controlled input value — the input itself should NOT be debounced, only the derived effect). Cases where useDeferredValue gives you the same UX with concurrent rendering.
⚠ Common pitfall: Returning a stale value on dependency change — make sure the cleanup clears the timer. Debounce delay too short is a no-op; too long feels broken. 200-500 ms is the sweet spot for search.
Toy hook for one-off GET requests. Returns { data, error, loading } and aborts in-flight requests when url or component unmounts. NOT a replacement for React Query / SWR — those handle cache, retry, dedupe.
✓ When to use
Prototypes, demos, internal tools where a real data library is overkill. Reading a single endpoint that does not need cache.
✗ When to avoid
Production apps. Endpoints that are read in multiple places (you will dedupe by hand). Anywhere caching, retry, or mutation matters.
⚠ Common pitfall: Without AbortController, a fast-changing url leaves you with race conditions where an OLDER response overwrites a newer one. Always abort in the cleanup.
Return whether a CSS media query currently matches. Subscribes via window.matchMedia and re-renders on change. Pair with useSyncExternalStore for concurrent safety.
✓ When to use
Render different components on mobile vs desktop. Toggle layouts at a breakpoint. Detect dark-mode preference via (prefers-color-scheme: dark).
✗ When to avoid
Pure styling — CSS media queries are cheaper than re-rendering React. Layout decisions that should be measured (use ResizeObserver instead).
⚠ Common pitfall: SSR returns false because window is undefined — that causes a layout flash. Wrap in useSyncExternalStore with a sane server snapshot, or render only on the client.
Wrapper around addEventListener that pairs the listener with the React lifecycle and uses a ref to keep the handler fresh without re-subscribing on every render.
✓ When to use
Keyboard shortcuts on window. scroll / resize / online-offline. Listening to a third-party element ref. Anywhere you would otherwise hand-roll useEffect + addEventListener + cleanup.
✗ When to avoid
React-managed elements — just use onClick / onKeyDown props. Heavy events (mousemove on document) without throttling.
⚠ Common pitfall: Without the handler-in-ref trick, every render re-subscribes and the listener cost balloons. Capture deps explicitly and ref the latest handler.
Run handler when a mousedown / touchstart fires OUTSIDE the referenced element. Canonical pattern for closing popovers, dropdowns, modals.
✓ When to use
Click-outside-to-close on a custom dropdown, popover, mention picker, tooltip.
✗ When to avoid
Native <dialog> elements (browsers handle Escape and outside-click for free). Components inside Portals — the ref-contains check fails because the portal child is outside the ref tree.
⚠ Common pitfall: click event fires too late (the click target may already be removed). Use mousedown / pointerdown / touchstart so the close happens BEFORE the next click handler runs.
Signatureconst previous = usePrevious<T>(value): T | undefined
Return the value from the PREVIOUS render. Implemented with a ref written in useEffect so the read during render returns the value from one render ago.
✓ When to use
Animating a transition between old and new value. Detecting a transition like "was open, now closed". Comparison with the last prop without holding redundant state.
✗ When to avoid
When you actually need to RENDER both values — useState or useReducer. When you need the previous of derived data — derive both and compare directly.
⚠ Common pitfall: Because the ref is set in useEffect AFTER render, on the very first render it returns undefined. Code must handle the undefined case explicitly.
Return live window dimensions. Subscribes to resize and updates on change. SSR-safe variant uses useSyncExternalStore with a server snapshot.
✓ When to use
Virtualized lists that need a viewport height. Canvas / SVG that draws to window dimensions. Conditional rendering above a breakpoint when CSS alone is not enough.
✗ When to avoid
Pure CSS responsive design — media queries are cheaper. Element-level sizing (use ResizeObserver, not window resize).
⚠ Common pitfall: resize fires at 60+ Hz on some platforms. Without throttling, the entire subtree re-renders on every pixel of drag. Throttle to 16-100 ms or use rAF batching.
SignatureuseInterval(callback: () => void, delayMs: number | null)
Declarative setInterval. Pass null as delay to pause. Uses a ref to always call the LATEST callback closure without re-creating the interval on every render.
✓ When to use
Polling status. Clock tick. Animation frame fallback. Any repeating action that should pause cleanly via state.
✗ When to avoid
Smooth animation (use requestAnimationFrame for vsync). One-shot timers (just useEffect + setTimeout). Sub-millisecond precision (browsers throttle to 4 ms minimum).
⚠ Common pitfall: A naive `useEffect(() => { setInterval(cb, d); }, [cb])` re-creates the interval whenever cb identity changes — usually every render. The ref-the-callback pattern is the fix Dan Abramov's blog made famous.
Searchable React hooks cheat sheet that covers all 17
built-in hooks shipped with React 18 + 19 plus ten of the
custom-hook patterns every codebase eventually grows. For
each hook you get the function signature, a one-paragraph
bilingual EN/ZH description, an explicit "when to use" vs
"when to avoid" pair, the one common pitfall that bites
real teams (stale closures in useEffect, missing
AbortController in useFetch, getSnapshot returning a fresh
object every render in useSyncExternalStore, calling
useFormStatus on the same component that owns the form),
and 1-3 copy-ready examples. State hooks (useState,
useReducer). Effect hooks (useEffect, useLayoutEffect,
useInsertionEffect — yes, that one too, including why you
almost never need it). Memoization (useMemo, useCallback —
with the anti-pattern callout that wrapping every value
makes things slower). Refs (useRef and the
useImperativeHandle + forwardRef pairing). Context
(useContext + the inline-object Provider trap). Concurrent
rendering (useTransition, useDeferredValue + the
React.memo requirement on heavy children). External
stores (useSyncExternalStore with concurrent-safe
bindings). Accessibility (useId and why its colons break
naive querySelector). The four React 19 additions
(useOptimistic, useActionState, useFormStatus, the
unified use() read primitive). And ten custom-hook
patterns straight out of production code: useToggle,
useLocalStorage, useDebounce, useFetch, useMediaQuery,
useEventListener, useOnClickOutside, usePrevious,
useWindowSize, useInterval. Filter by built-in vs custom,
search across name / signature / description / examples
/ pitfalls simultaneously, one-click copy on every
signature. Pure client-side, no upload, no tracking.
Tool details
Input
Files
The page exposes text boxes, numeric controls, file pickers, or structured inputs depending on the tool.
Output
Live result + Copy + Preview
The result area focuses on usable output, with copy, download, or preview actions when supported.
Privacy
May use a live lookup
A network call is detected in the component, so redact sensitive data when appropriate.
Save / share
Local preference storage
Preferences, history, or drafts are saved in this browser without an account.
Performance budget
Initial JS <= 30 KB
No WASM budget is declared, keeping the tool quick to open on mobile.
Best fit
Developer & DevOps · Developer
Category and role tags drive related tools, internal links, and quick fit checks.
How to use
1
1. Input
Paste or drop your content into the tool panel.
2
2. Process
Click the button. All processing is local in your browser.
3
3. Copy / Download
Copy the result or download to disk in one click.
How React Hooks Cheatsheet fits into your work
Use it in the small gaps between coding, reviewing, debugging, and shipping.
Developer jobs
Formatting, validating, shrinking, or inspecting code-adjacent text.
Preparing snippets for documentation, tickets, commits, or handoff.
Checking a small payload quickly without switching tools.
Developer checks
Run irreversible transforms like minify or obfuscate on a copy.
Keep secrets out of pasted snippets unless the tool explicitly stays local.
Use your normal tests or linter before shipping transformed code.
Good next steps
These links move the current task into a more complete workflow.