Toolmingo
Guides9 min read

React Hooks Cheat Sheet: All Built-In Hooks with Examples

A complete React hooks cheat sheet — useState, useEffect, useRef, useMemo, useCallback, useContext, useReducer, and more. Copy-ready examples with common patterns and pitfalls.

React hooks let you use state and lifecycle features inside function components. This reference covers every built-in hook with practical examples, common patterns, and the pitfalls that trip up most developers.

Quick reference

The 20 patterns that cover 95% of everyday React hook usage.

Pattern Hook What it does
const [n, setN] = useState(0) useState Local state
const [s, setS] = useState<T>(init) useState Typed state
setState(prev => prev + 1) useState Functional update
useEffect(() => {}, []) useEffect Run once on mount
useEffect(() => {}, [dep]) useEffect Run when dep changes
useEffect(() => () => cleanup()) useEffect With cleanup
const ref = useRef(null) useRef DOM reference
const count = useRef(0) useRef Mutable value (no re-render)
const val = useMemo(() => compute(), [dep]) useMemo Memoize value
const fn = useCallback(() => {}, [dep]) useCallback Memoize function
const ctx = useContext(MyCtx) useContext Read context
const [s, dispatch] = useReducer(fn, init) useReducer Complex state logic
useLayoutEffect(() => {}, []) useLayoutEffect Before paint
useId() useId Unique ID
useTransition() useTransition Non-urgent updates
useDeferredValue(val) useDeferredValue Defer expensive re-renders
useImperativeHandle(ref, () => ({})) useImperativeHandle Expose ref methods
useDebugValue(label) useDebugValue DevTools label
useSyncExternalStore(sub, snap) useSyncExternalStore External store
useInsertionEffect(() => {}) useInsertionEffect CSS-in-JS injection

useState — local state

The most-used hook. Stores a value and re-renders the component when it changes.

import { useState } from "react";

// Primitive
const [count, setCount] = useState(0);

// TypeScript typed
const [name, setName] = useState<string>("");

// Object state
const [user, setUser] = useState<{ name: string; age: number } | null>(null);

// Lazy initializer (runs once, useful for expensive defaults)
const [items, setItems] = useState<string[]>(() => {
  return JSON.parse(localStorage.getItem("items") ?? "[]");
});

Functional updates — always use when new state depends on old:

// ✅ Correct — uses previous value
setCount(prev => prev + 1);

// ❌ Wrong inside async/batch — may use stale closure value
setCount(count + 1);

Object state — always spread, never mutate:

// ✅ Immutable update
setUser(prev => ({ ...prev!, age: 30 }));

// ❌ Mutation — React won't re-render
user.age = 30; // direct mutation
setUser(user);  // same reference, no re-render

useEffect — side effects

Runs side effects (data fetching, subscriptions, DOM manipulation) after render.

import { useEffect, useState } from "react";

// Run once on mount (empty deps)
useEffect(() => {
  document.title = "Hello";
}, []);

// Run when dep changes
useEffect(() => {
  fetchUser(userId);
}, [userId]);

// Cleanup — returned function runs on unmount or before next run
useEffect(() => {
  const id = setInterval(() => setTick(t => t + 1), 1000);
  return () => clearInterval(id);  // cleanup
}, []);

// Data fetching pattern with AbortController
useEffect(() => {
  const controller = new AbortController();

  async function load() {
    try {
      const res = await fetch(`/api/users/${id}`, { signal: controller.signal });
      const data = await res.json();
      setUser(data);
    } catch (err) {
      if ((err as Error).name !== "AbortError") setError(err as Error);
    }
  }

  load();
  return () => controller.abort();
}, [id]);

Dependency rules — include everything the effect reads from the component scope:

// ✅ Correct deps
useEffect(() => {
  fetchData(userId, filter);
}, [userId, filter]);

// ❌ Missing deps — stale closure bug
useEffect(() => {
  fetchData(userId, filter); // uses stale filter
}, [userId]);               // eslint-plugin-react-hooks will warn

useRef — mutable refs

Two main uses: accessing DOM nodes, and storing mutable values without causing re-renders.

import { useRef, useEffect } from "react";

// DOM reference
function AutoFocus() {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    inputRef.current?.focus();
  }, []);

  return <input ref={inputRef} />;
}

// Mutable value — does NOT trigger re-render on change
function Timer() {
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);

  function start() {
    intervalRef.current = setInterval(() => console.log("tick"), 1000);
  }
  function stop() {
    if (intervalRef.current) clearInterval(intervalRef.current);
  }

  return <button onClick={start}>Start</button>;
}

// Track previous value
function usePrevious<T>(value: T) {
  const ref = useRef<T>(value);
  useEffect(() => { ref.current = value; });
  return ref.current; // previous render's value
}

useMemo — memoize values

Caches an expensive computed value; recomputes only when deps change.

import { useMemo } from "react";

const sortedList = useMemo(() => {
  return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]);

// Expensive filter
const filtered = useMemo(() =>
  products.filter(p => p.price < maxPrice && p.category === cat),
  [products, maxPrice, cat]
);

When to use: only when the computation is genuinely expensive (sorting/filtering large arrays, complex math). For cheap calculations, useMemo adds overhead without benefit.


useCallback — memoize functions

Returns a stable function reference. Useful when passing callbacks to memoized children to prevent unnecessary re-renders.

import { useCallback } from "react";

const handleSubmit = useCallback(async (data: FormData) => {
  await api.post("/submit", data);
  onSuccess?.();
}, [onSuccess]); // recreated only when onSuccess changes

// Pair with React.memo to avoid child re-renders
const Button = React.memo(({ onClick }: { onClick: () => void }) => (
  <button onClick={onClick}>Click</button>
));

When to use: when a function is a dep of useEffect, or a prop to React.memo wrapped children. Don't wrap every function — it's premature optimisation.


useContext — read context

Reads the nearest context value above in the tree. Component re-renders when the context value changes.

import { createContext, useContext, useState } from "react";

// 1. Create context with a default
const ThemeCtx = createContext<"light" | "dark">("light");

// 2. Provide context high up in the tree
function App() {
  const [theme, setTheme] = useState<"light" | "dark">("light");
  return (
    <ThemeCtx.Provider value={theme}>
      <Page />
    </ThemeCtx.Provider>
  );
}

// 3. Consume anywhere below
function Page() {
  const theme = useContext(ThemeCtx);
  return <div className={theme}>Hello</div>;
}

// Custom hook pattern (recommended)
function useTheme() {
  const ctx = useContext(ThemeCtx);
  if (!ctx) throw new Error("useTheme must be inside ThemeCtx.Provider");
  return ctx;
}

useReducer — complex state

Alternative to useState for complex state logic with multiple sub-values or when next state depends on previous.

import { useReducer } from "react";

type State = { count: number; status: "idle" | "loading" | "error" };
type Action =
  | { type: "increment" }
  | { type: "reset" }
  | { type: "setLoading" }
  | { type: "setError" };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "increment": return { ...state, count: state.count + 1 };
    case "reset":     return { count: 0, status: "idle" };
    case "setLoading": return { ...state, status: "loading" };
    case "setError":  return { ...state, status: "error" };
    default: return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0, status: "idle" });

  return (
    <div>
      <p>Count: {state.count} ({state.status})</p>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
    </div>
  );
}

useReducer vs useState: use useReducer when you have more than 2-3 related state fields, or when state transitions are complex and benefit from being explicit.


useLayoutEffect — before paint

Same as useEffect but fires synchronously after all DOM mutations, before the browser paints. Use for DOM measurements.

import { useLayoutEffect, useRef, useState } from "react";

function Tooltip() {
  const ref = useRef<HTMLDivElement>(null);
  const [width, setWidth] = useState(0);

  // Runs before paint — no flicker
  useLayoutEffect(() => {
    if (ref.current) setWidth(ref.current.offsetWidth);
  }, []);

  return <div ref={ref}>Width: {width}px</div>;
}

When to use: only when you need to measure DOM before it's shown. For everything else, use useEffect.


useId — stable unique IDs

Generates a unique ID that is stable across server and client renders (for SSR/hydration).

import { useId } from "react";

function Field({ label }: { label: string }) {
  const id = useId();
  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input id={id} />
    </div>
  );
}

Don't use for list keys (key prop). Use for ARIA associations and form labels.


useTransition — concurrent rendering

Marks state updates as non-urgent so React can interrupt them for higher-priority updates (user input stays responsive).

import { useState, useTransition } from "react";

function SearchPage() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState<string[]>([]);
  const [isPending, startTransition] = useTransition();

  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    setQuery(e.target.value); // urgent — update input immediately

    startTransition(() => {
      // non-urgent — can be interrupted
      setResults(filterLargeList(e.target.value));
    });
  }

  return (
    <>
      <input value={query} onChange={handleChange} />
      {isPending && <Spinner />}
      <ResultList items={results} />
    </>
  );
}

Custom hooks — reusable logic

Extract stateful logic into a function whose name starts with use. The function can call other hooks.

// useLocalStorage
function useLocalStorage<T>(key: string, initialValue: T) {
  const [stored, setStored] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? (JSON.parse(item) as T) : initialValue;
    } catch {
      return initialValue;
    }
  });

  function setValue(value: T | ((prev: T) => T)) {
    const next = value instanceof Function ? value(stored) : value;
    setStored(next);
    window.localStorage.setItem(key, JSON.stringify(next));
  }

  return [stored, setValue] as const;
}

// useWindowSize
function useWindowSize() {
  const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight });

  useEffect(() => {
    const handler = () => setSize({ width: window.innerWidth, height: window.innerHeight });
    window.addEventListener("resize", handler);
    return () => window.removeEventListener("resize", handler);
  }, []);

  return size;
}

// useDebounce
function useDebounce<T>(value: T, delay: number): T {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);
  }, [value, delay]);

  return debounced;
}

6 common mistakes

1. Missing useEffect dependencies

// ❌ Stale closure — filter never updates
useEffect(() => { fetchData(filter); }, []);

// ✅ Include all deps
useEffect(() => { fetchData(filter); }, [filter]);

2. Calling hooks conditionally

// ❌ Breaks rules of hooks — always call in the same order
if (condition) {
  const [x, setX] = useState(0); // React will throw
}

// ✅ Condition inside the hook, not around it
const [x, setX] = useState(0);
if (condition) { /* use x */ }

3. Mutating state directly

// ❌ No re-render triggered
items.push(newItem);
setItems(items);

// ✅ New array
setItems(prev => [...prev, newItem]);

4. Forgetting cleanup in useEffect

// ❌ Memory leak — listener piles up on every render
useEffect(() => {
  window.addEventListener("resize", handler);
});

// ✅ Return cleanup
useEffect(() => {
  window.addEventListener("resize", handler);
  return () => window.removeEventListener("resize", handler);
}, []);

5. useEffect for derived state

// ❌ Unnecessary effect + extra render
const [fullName, setFullName] = useState("");
useEffect(() => {
  setFullName(`${first} ${last}`);
}, [first, last]);

// ✅ Compute during render (no hook needed)
const fullName = `${first} ${last}`;

6. Overusing useMemo / useCallback

// ❌ Premature optimization — memoizing a cheap operation
const doubled = useMemo(() => count * 2, [count]);

// ✅ Just compute it
const doubled = count * 2;

FAQ

What are the rules of hooks?

Only call hooks at the top level (not inside loops, conditions, or nested functions). Only call hooks from React function components or custom hooks.

What's the difference between useEffect and useLayoutEffect?

useEffect runs asynchronously after the browser paints — no visual delay. useLayoutEffect runs synchronously before paint — use for DOM measurements to prevent flicker.

When should I use useReducer instead of useState?

When you have 3+ related pieces of state, complex transitions, or when the next state depends heavily on the previous one. useReducer makes state logic testable in isolation.

Why does my useEffect run twice in development?

React 18 Strict Mode intentionally mounts, unmounts, and remounts components in development to help you find missing cleanups. In production it runs once. Fix: add proper cleanup.

What's the difference between useRef and useState?

useState re-renders the component on change. useRef does not — it's a box whose .current you can mutate freely. Use refs for values that don't affect the UI (timer IDs, previous values, DOM nodes).

Can I use async functions directly in useEffect?

No — useEffect must return either nothing or a cleanup function. Instead, define an async function inside the effect and call it:

useEffect(() => {
  async function load() {
    const data = await fetchData();
    setData(data);
  }
  load();
}, []);

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools