Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

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 is memo
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} /> then inputRef.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 ref like any prop — no forwardRef needed
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 inside if and 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 htmlFor and aria-*, 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: pending of the enclosing form — call it in a child component
<form action={fn}>
React calls fn with the FormData and 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-hooks and exhaustive-deps catch most of the bugs on this page
key={id} to reset
a different key remounts 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.