React Hooks Cheat Sheet: Every Hook With a Live Example | How-to Guide

React hooks are functions such as useState, useEffect and useRef that give function components state, side effects and refs. This cheat sheet covers every built-in hook in React 19.3 with a one-line purpose, a live example and its most common mistake, plus 12 custom hooks you can copy.

Every built-in hook at a glance

At the time of writing (October 2026) the latest React release is 19.3. The table lists every hook on the official React hooks reference plus the three React 19 hooks documented on their own pages (use, useOptimistic and the React DOM hook useFormStatus). Hooks marked React 18 work in React 18 and 19. Hooks marked React 19 do not exist in React 18. Every React 18 hook below has a live example you can click; the React 19 ones are shown as code only, because this page runs on React 18.

Showing all 20
HookGroupUse it toSince
useStateStatehold a value that re-renders the component when it changesReact 18
useReducerStatemanage state with many related updates through one reducer functionReact 18
useContextContextread a value from the nearest provider aboveReact 18
useRefRefkeep a mutable value or a DOM node that does not trigger rendersReact 18
useImperativeHandleRefchoose which methods a parent can call through a refReact 18
useEffectEffectsync with something outside React after paintReact 18
useLayoutEffectEffectmeasure layout before the browser paintsReact 18
useInsertionEffectEffectinsert style rules before layout effects run (CSS-in-JS libraries)React 18
useEffectEventEffectread the latest props inside an effect without re-running itReact 19.2
useMemoPerformancecache an expensive calculation between rendersReact 18
useCallbackPerformancecache a function so memoized children do not re-renderReact 18
useTransitionPerformancemark a state update as non-urgent and get a pending flagReact 18
useDeferredValuePerformancelet a slow part of the UI lag behind fast typingReact 18
useIdOthergenerate stable unique IDs for labels and ARIA attributesReact 18
useSyncExternalStoreOthersubscribe to a store that lives outside ReactReact 18
useDebugValueOtherlabel a custom hook in React DevToolsReact 18
useActionStateOthertrack the result and pending state of a form actionReact 19
useOptimisticOthershow the expected result of an action before it finishesReact 19
useAPIread a promise or context, even inside conditionsReact 19
useFormStatusReact DOMread the pending state of the parent formReact 19

Hooks follow two rules: call them only at the top level of a component or custom hook (never inside loops, conditions or nested functions), and call them only from React function components or other hooks. use is the one exception to the first rule: it may be called inside if blocks and loops. The eslint-plugin-react-hooks package checks both rules for you.

State hooks

useState

What it does: Stores a value for this component. Calling the setter schedules a re-render with the new value.

import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <>
      <button onClick={() => setCount((c) => c - 1)}>-1</button>
      <span>{count}</span>
      <button onClick={() => setCount((c) => c + 1)}>+1</button>
    </>
  );
}

Common mistake: Writing setCount(count + 1) three times in one click and expecting +3. All three read the same count from this render, so you get +1. Pass an updater function, setCount(c => c + 1), when the next value depends on the previous one. The "+3 (updater)" button above does exactly that. A second classic: mutating an object or array in place (user.name = "x"; setUser(user)). React compares by reference, sees the same object and skips the render. Create a new object instead: setUser({ ...user, name: "x" }).

useReducer

What it does: Moves update logic into one pure function that takes the current state and an action and returns the next state. Useful when several values change together.

type Action = { type: "add"; text: string } | { type: "toggle"; id: number };

function reducer(state: Todo[], action: Action): Todo[] {
  switch (action.type) {
    case "add":
      return [...state, { id: Date.now(), text: action.text, done: false }];
    case "toggle":
      return state.map((t) => (t.id === action.id ? { ...t, done: !t.done } : t));
  }
}

const [todos, dispatch] = useReducer(reducer, []);
dispatch({ type: "add", text: "Ship it" });

Common mistake: Doing side effects inside the reducer (fetching, writing to localStorage, generating random IDs from a timer you rely on later). In development Strict Mode React calls reducers twice to expose impure code. Keep the reducer pure and put side effects in event handlers or effects.

Context hooks

useContext

What it does: Reads the value of the closest matching provider above this component, and re-renders when that value changes.

const ThemeContext = createContext<"light" | "dark">("light");

function App() {
  const [theme, setTheme] = useState<"light" | "dark">("light");
  return (
    <ThemeContext.Provider value={theme}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Badge() {
  const theme = useContext(ThemeContext);
  return <span className={theme}>Current theme: {theme}</span>;
}

Common mistake: Passing a new object literal as the provider value on every render, such as value={{ user, setUser }}. Each render creates a new object, so every consumer re-renders even when nothing changed. Wrap the value in useMemo, or split rarely-changing and often-changing values into two contexts. Also check that the provider sits above the component: calling useContext in the same component that renders the provider returns the default value.

Ref hooks

useRef

What it does: Gives you a box (ref.current) that survives re-renders. Changing it does not cause a render. Use it for DOM nodes, timer IDs and other values the screen does not show.

const inputRef = useRef<HTMLInputElement>(null);
const clicks = useRef(0);

<input ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>Focus</button>
<button onClick={() => { clicks.current += 1; }}>Count silently</button>

Common mistake: Showing ref.current in the JSX and expecting it to update. Refs do not trigger renders, so the screen only catches up when something else re-renders (try the "Add to ref" button, then "Re-render" above). If the value should appear on screen, it belongs in state. Also avoid reading or writing refs during render, except for lazy one-time initialization.

useImperativeHandle

What it does: Customizes what a parent receives when it puts a ref on your component, so you expose a few named methods instead of the whole DOM node.

type Handle = { shake: () => void; clear: () => void };

// React 18: wrap in forwardRef. In React 19 you can accept ref as a normal prop.
const FancyInput = forwardRef<Handle, Props>(function FancyInput(props, ref) {
  const el = useRef<HTMLInputElement>(null);
  useImperativeHandle(ref, () => ({
    shake() { /* animate */ el.current?.focus(); },
    clear() { if (el.current) el.current.value = ""; },
  }), []);
  return <input ref={el} />;
});

// parent
const ref = useRef<Handle>(null);
<FancyInput ref={ref} />
<button onClick={() => ref.current?.shake()}>Shake</button>

Common mistake: Using it to drive things that props can express. If a parent calls ref.current.open() to show a modal, an isOpen prop is usually simpler and easier to follow. Keep imperative handles for focus, scroll, play/pause and animations. Forgetting the dependency array (the third argument) also recreates the handle on every render.

Effect hooks

useEffect

What it does: Runs code after React has updated the screen, to keep a component in sync with something outside React: a timer, a subscription, a network request, a non-React widget. Return a cleanup function to undo it.

useEffect(() => {
  if (!running) return;
  const id = setInterval(() => setSeconds((s) => s + 1), 1000);
  return () => clearInterval(id); // runs before the next effect and on unmount
}, [running]);

Common mistake: Using an effect to compute something you could calculate during render, such as useEffect(() => setFullName(first + " " + last), [first, last]). That costs an extra render and can show stale values for a frame. Write const fullName = first + " " + last instead. The second most common bug is a missing cleanup: in development Strict Mode React mounts, unmounts and remounts each component once, so an effect without cleanup will subscribe or start a timer twice. That double run is a hint, not a bug in React.

useLayoutEffect

What it does: Same shape as useEffect, but runs after React changes the DOM and before the browser paints. Use it to measure an element and adjust layout so users never see the wrong position.

const boxRef = useRef<HTMLSpanElement>(null);
const [width, setWidth] = useState(0);

useLayoutEffect(() => {
  setWidth(boxRef.current!.getBoundingClientRect().width);
}, [text]);

Common mistake: Reaching for it by default. It blocks painting, so slow work inside it makes the page feel stuck. Use useEffect unless you can see a flicker that only a pre-paint measurement fixes (tooltips, popover positioning, auto-sizing). It also does nothing during server rendering, which produces a warning in some setups.

useInsertionEffect

What it does: Runs before any layout effects so a library can insert <style> rules before anything reads layout. It exists for authors of CSS-in-JS libraries.

function useScopedRule(selector: string, rule: string) {
  useInsertionEffect(() => {
    const style = document.createElement("style");
    style.textContent = `${selector} { ${rule} }`;
    document.head.appendChild(style);
    return () => style.remove();
  }, [selector, rule]);
}

Common mistake: Using it in application code, or trying to update state from it. You cannot schedule updates inside it and refs are not attached yet. If you are not writing a styling library, you almost certainly want useEffect or useLayoutEffect.

useEffectEvent

What it does: React 19.2 and later. Wraps a function so an effect can call it and always see the latest props and state, without listing those values as dependencies. Use it for the "event" part of an effect, such as logging or showing a notification with the current theme.

// React 19.2+ only. Not run on this page.
import { useEffect, useEffectEvent } from "react";

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification("Connected", theme); // always the latest theme
  });

  useEffect(() => {
    const conn = createConnection(roomId);
    conn.on("connected", () => onConnected());
    conn.connect();
    return () => conn.disconnect();
  }, [roomId]); // theme is not a dependency, and onConnected must not be listed
}

Common mistake: Putting the effect event in the dependency array, or calling it during render or passing it to other components. React's documentation says to call effect events only from inside effects (or other effect events), and its lint rule warns when you list one as a dependency. On React 18 the usual stand-in is the "latest ref" pattern used by useInterval further down this page.

Performance hooks

useMemo

What it does: Caches the result of a calculation and only recalculates when its dependencies change.

const filtered = useMemo(
  () => items.filter((item) => item.includes(query)),
  [items, query],
);

Common mistake: Wrapping everything in it. Memoizing cheap work like a + b or a short map adds code and comparison cost for no gain. Measure first (React DevTools Profiler) and memoize slow calculations, values passed to memo children, and values used as effect dependencies. A related trap: a dependency that is a new object every render ({ sort: "asc" }) makes the memo recompute every time.

useCallback

What it does: Returns the same function instance between renders until a dependency changes. It matters when the function is passed to a child wrapped in memo, or used as an effect dependency.

const handleAdd = useCallback(() => setCount((c) => c + 1), []);

const Child = memo(function Child({ onClick }: { onClick: () => void }) {
  return <button onClick={onClick}>Add</button>;
});

<Child onClick={handleAdd} />

Common mistake: Using useCallback when the child is not memoized. Without memo on the child, it re-renders with its parent anyway, so the stable function changes nothing. The live demo shows both children: only the one receiving the cached function skips renders. Stale closures are the other trap: if the callback reads count but the dependency list is empty, it keeps seeing the first value. Use an updater function or list the dependency.

useTransition

What it does: Marks a state update as non-urgent. React keeps the current screen responsive while it prepares the new one, and gives you an isPending flag for a loading hint.

const [isPending, startTransition] = useTransition();

function selectTab(next: string) {
  startTransition(() => {
    setTab(next);
  });
}

Common mistake: Wrapping a text input's own state in a transition. Controlled inputs must update urgently, or typing feels broken. Keep the input state normal and move the slow, derived update into the transition (or use useDeferredValue). In React 18 the function passed to startTransition must be synchronous; React 19 also accepts async functions.

useDeferredValue

What it does: Gives you a copy of a value that is allowed to lag behind. Fast parts of the UI use the real value; a slow part renders with the deferred one and catches up when the browser is idle.

const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;

<input value={query} onChange={(e) => setQuery(e.target.value)} />
<div style={{ opacity: isStale ? 0.5 : 1 }}>
  <SlowResults query={deferredQuery} />
</div>

Common mistake: Forgetting to wrap the slow child in memo. If the child re-renders with the parent on every keystroke anyway, deferring its prop does not help. It is also not a debounce: it does not wait a fixed time and it does not reduce network requests. For fewer requests, debounce (see useDebounce below).

Other hooks

useId

What it does: Generates an ID that is unique within the app and identical on the server and the client, for htmlFor, aria-describedby and similar attributes.

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

Common mistake: Using it to make list keys. Keys should come from your data (an ID from the database), not from a hook. Also avoid Math.random() for element IDs in server-rendered apps: the server and browser produce different numbers and hydration fails, which is the problem useId solves.

useSyncExternalStore

What it does: Subscribes a component to a data source React does not own (a browser API, a global store) and reads a snapshot that stays consistent during concurrent rendering.

function subscribe(callback: () => void) {
  window.addEventListener("online", callback);
  window.addEventListener("offline", callback);
  return () => {
    window.removeEventListener("online", callback);
    window.removeEventListener("offline", callback);
  };
}

const online = useSyncExternalStore(
  subscribe,
  () => navigator.onLine, // client snapshot
  () => true,             // server snapshot
);

Common mistake: Defining subscribe inline inside the component. A new function each render makes React unsubscribe and resubscribe every time. Define it outside the component or wrap it in useCallback. The snapshot function must also return the same value when nothing changed: returning a fresh object or array each call causes an infinite render loop.

useDebugValue

What it does: Adds a label to a custom hook in React DevTools. It has no effect on what users see.

function useOnlineStatus() {
  const online = useSyncExternalStore(subscribe, () => navigator.onLine);
  useDebugValue(online ? "Online" : "Offline");
  return online;
}

Common mistake: Adding it to every custom hook or doing expensive formatting in it. Use it for shared hooks whose state is hard to read in DevTools, and pass a formatter as the second argument if the label is costly to build: useDebugValue(date, d => d.toISOString()).

React 19 hooks (code only, not runnable on React 18)

React 19 only. The four hooks in this section (use, useActionState, useOptimistic, useFormStatus) are not exported by React 18. This page itself runs React 18, so they are shown as code you can copy into a React 19 project, without live demos. useEffectEvent, covered above, needs React 19.2.

useActionState

What it does: Wraps a form action and gives you its latest result plus a pending flag. The action receives the previous state and the submitted FormData.

// React 19+
import { useActionState } from "react";

async function subscribe(prev: { error?: string; ok?: boolean }, formData: FormData) {
  const email = String(formData.get("email") ?? "");
  if (!email.includes("@")) return { error: "Enter a valid email" };
  await saveEmail(email);
  return { ok: true };
}

function Newsletter() {
  const [state, formAction, isPending] = useActionState(subscribe, {});
  return (
    <form action={formAction}>
      <input name="email" type="email" />
      <button disabled={isPending}>{isPending ? "Saving..." : "Subscribe"}</button>
      {state.error && <p role="alert">{state.error}</p>}
      {state.ok && <p>Thanks, you are subscribed.</p>}
    </form>
  );
}

Common mistake: Forgetting that the action's first argument is the previous state. Writing async function subscribe(formData) means formData is actually the old state and .get throws. Also note that it was briefly called useFormState (in react-dom) during the React 19 canary period; current docs use useActionState from react.

useOptimistic

What it does: Shows a temporary, optimistic version of state while an async action runs, then falls back to the real state when it finishes.

// React 19+
import { useOptimistic } from "react";

function Thread({ messages, send }: Props) {
  const [optimistic, addOptimistic] = useOptimistic(
    messages,
    (current, text: string) => [...current, { text, sending: true }],
  );

  async function action(formData: FormData) {
    const text = String(formData.get("text"));
    addOptimistic(text);
    await send(text); // parent updates `messages` when this resolves
  }

  return (
    <form action={action}>
      {optimistic.map((m, i) => <p key={i}>{m.text}{m.sending && " (sending)"}</p>)}
      <input name="text" />
    </form>
  );
}

Common mistake: Calling the optimistic setter outside an action or transition. React expects it inside a form action or startTransition; elsewhere the optimistic value appears and vanishes immediately (React warns in development). And always handle failure: if send throws, show an error so the message does not silently disappear.

use

What it does: Reads the value of a promise (suspending until it resolves) or a context. Unlike other hooks, it can be called inside conditions and loops.

// React 19+
import { use, Suspense } from "react";

function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
  const comments = use(commentsPromise); // suspends until resolved
  return comments.map((c) => <p key={c.id}>{c.text}</p>);
}

function Page({ commentsPromise }) {
  return (
    <Suspense fallback={<p>Loading comments...</p>}>
      <Comments commentsPromise={commentsPromise} />
    </Suspense>
  );
}

// Conditional context read, which useContext cannot do:
function Heading({ show }: { show: boolean }) {
  if (!show) return null;
  const theme = use(ThemeContext);
  return <h1 className={theme}>Hello</h1>;
}

Common mistake: Creating the promise inside the component that calls use, such as use(fetch(url)). Each render makes a new promise, so the component suspends forever. Create the promise once (in a server component, a loader, a cache, or a parent) and pass it down. use also cannot go inside try/catch; handle errors with an error boundary.

useFormStatus

What it does: Imported from react-dom. Reads the status of the parent <form>: pending, data, method and action.

// React 19+
import { useFormStatus } from "react-dom";

function SubmitButton() {
  const { pending } = useFormStatus();
  return <button type="submit" disabled={pending}>{pending ? "Sending..." : "Send"}</button>;
}

function ContactForm() {
  return (
    <form action={sendMessage}>
      <textarea name="message" />
      <SubmitButton />
    </form>
  );
}

Common mistake: Calling it in the same component that renders the <form>. It only reports on a form above the calling component, so in the form's own component pending is always false. Move the button into its own component, as above.

The dependency array, explained

useEffect, useLayoutEffect, useMemo, useCallback and useImperativeHandle take a list of values as their last argument. React compares each item with the value from the previous render using Object.is. If any item changed, the effect runs again (or the memo recalculates). That single rule explains every case:

You writeThe effect runsTypical use
useEffect(fn)after every renderrare; usually a sign something should be computed during render
useEffect(fn, [])once after mount (twice in development Strict Mode), cleanup on unmountsubscriptions and setup that use no props or state
useEffect(fn, [a, b])after mount, then whenever a or b changesfetching by ID, syncing to a prop

Click the buttons: changing b re-runs only the effect with no array, changing a re-runs both that one and the [a] effect, and the [] effect stays quiet after mount.

Five rules that prevent most dependency bugs

  1. List every reactive value the callback reads. Props, state, and anything declared in the component body. Leaving one out gives you a stale closure: the callback keeps the value from an old render. Install eslint-plugin-react-hooks and let exhaustive-deps tell you what is missing.
  2. Objects and functions created during render are new every time. { id } or () => {} never equals the previous one under Object.is, so they re-run the effect every render. Move them inside the effect, move them outside the component, or memoize them.
  3. Depend on primitives where you can. [user.id] is more stable than [user] when only the ID matters.
  4. Use updater functions to drop state dependencies. setCount(c => c + 1) does not read count, so count does not need to be listed.
  5. Do not silence the linter to "fix" a loop. An infinite loop usually means the effect sets state that is also a dependency, or depends on a fresh object. Fix the cause. For values you want to read without reacting to them, use useEffectEvent on React 19.2+, or a ref that you keep up to date on React 18.
// Stale closure: logs 0 forever
useEffect(() => {
  const id = setInterval(() => console.log(count), 1000);
  return () => clearInterval(id);
}, []); // count is missing

// Fixed: re-subscribe when count changes
useEffect(() => {
  const id = setInterval(() => console.log(count), 1000);
  return () => clearInterval(id);
}, [count]);

// Fetch with cleanup so a slow old response cannot overwrite a newer one
useEffect(() => {
  const controller = new AbortController();
  fetch(`/api/users/${userId}`, { signal: controller.signal })
    .then((r) => r.json())
    .then(setUser)
    .catch((err) => { if (err.name !== "AbortError") setError(err); });
  return () => controller.abort();
}, [userId]);

12 custom hooks to copy into your project

A custom hook is a function whose name starts with use and that calls other hooks. It lets several components share stateful logic; each component that calls it gets its own independent state. The hooks below are written for this page in TypeScript, work on React 18 and 19, and have no dependencies. Each one ends with a note on when not to use it.

1. useDebounce

Returns a value that only updates after the input has stopped changing for delay milliseconds. Good for search boxes that call an API.

import { useEffect, useState } from "react";

export function useDebounce<T>(value: T, delay = 300): T {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = window.setTimeout(() => setDebounced(value), delay);
    return () => window.clearTimeout(id);
  }, [value, delay]);
  return debounced;
}

// const debouncedQuery = useDebounce(query, 400);
// useEffect(() => { search(debouncedQuery); }, [debouncedQuery]);

When not to use it: When you only need the UI to stay responsive while rendering a big list. That is a rendering problem, and useDeferredValue solves it without an artificial delay. Also skip it for form validation that should feel instant.

2. useLocalStorage

Works like useState but saves the value in localStorage and restores it on the next visit.

import { useEffect, useState } from "react";

export function useLocalStorage<T>(key: string, initial: T) {
  const [value, setValue] = useState<T>(() => {
    try {
      const raw = window.localStorage.getItem(key);
      return raw === null ? initial : (JSON.parse(raw) as T);
    } catch {
      return initial; // private mode, blocked storage or bad JSON
    }
  });

  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch {
      /* storage full or blocked: keep the in-memory value */
    }
  }, [key, value]);

  return [value, setValue] as const;
}

When not to use it: For anything sensitive (tokens, personal data), since any script on the page can read localStorage. Also not for server-rendered pages without care: the server has no window, so read storage after mount to avoid a hydration mismatch. It does not sync between tabs; add a storage event listener if you need that.

3. useMediaQuery

Returns true while a CSS media query matches, and updates when it changes.

import { useCallback, useSyncExternalStore } from "react";

export function useMediaQuery(query: string): boolean {
  const subscribe = useCallback((onChange: () => void) => {
    const mql = window.matchMedia(query);
    mql.addEventListener("change", onChange);
    return () => mql.removeEventListener("change", onChange);
  }, [query]);

  return useSyncExternalStore(
    subscribe,
    () => window.matchMedia(query).matches,
    () => false, // server snapshot
  );
}

// const isDesktop = useMediaQuery("(min-width: 1024px)");
// const reduceMotion = useMediaQuery("(prefers-reduced-motion: reduce)");

When not to use it: For layout changes CSS can make by itself. Hiding or rearranging elements with Tailwind breakpoints (md:hidden) works before JavaScript loads and never flashes. Use the hook when you must render different components or skip loading something entirely.

4. useToggle

A boolean with a stable toggle function, for menus, modals and disclosure sections.

import { useCallback, useState } from "react";

export function useToggle(initial = false) {
  const [on, setOn] = useState(initial);
  const toggle = useCallback(() => setOn((v) => !v), []);
  return [on, toggle, setOn] as const;
}

// const [open, toggleOpen, setOpen] = useToggle();

When not to use it: When the state has more than two values (closed, opening, open, error). Use a string union with useState or useReducer instead of stacking several toggles.

5. usePrevious

Returns the value from the previous render, handy for showing a change ("up from 12") or reacting to a transition.

import { useEffect, useRef } from "react";

export function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T>();
  useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
}

When not to use it: To decide whether to update state. React's documentation shows a cleaner pattern for that: store the previous value in state and compare during render. This hook reads a ref during render, which works today but is the kind of code React's guidance asks you to keep rare.

6. useOnClickOutside

Calls a handler when the user presses anywhere outside a given element. Typical for closing a custom menu or popover.

import { useEffect, useRef, type RefObject } from "react";

export function useOnClickOutside(
  ref: RefObject<HTMLElement>,
  handler: (event: PointerEvent) => void,
) {
  const saved = useRef(handler);
  useEffect(() => { saved.current = handler; }, [handler]);

  useEffect(() => {
    const listener = (event: PointerEvent) => {
      if (!ref.current || ref.current.contains(event.target as Node)) return;
      saved.current(event);
    };
    document.addEventListener("pointerdown", listener);
    return () => document.removeEventListener("pointerdown", listener);
  }, [ref]);
}

When not to use it: If you are already using a headless library (Radix, Headless UI) for the menu or dialog: they handle outside clicks, Escape and focus return for you. A hand-made menu built only on this hook usually misses keyboard support.

7. useInterval

A setInterval that always calls the latest callback and can be paused by passing null as the delay.

import { useEffect, useRef } from "react";

export function useInterval(callback: () => void, delay: number | null) {
  const saved = useRef(callback);
  useEffect(() => { saved.current = callback; }, [callback]);

  useEffect(() => {
    if (delay === null) return;
    const id = window.setInterval(() => saved.current(), delay);
    return () => window.clearInterval(id);
  }, [delay]);
}

// useInterval(() => setTicks((t) => t + 1), running ? 1000 : null);

When not to use it: For animations (use CSS or requestAnimationFrame) or for accurate clocks: intervals drift and browsers slow them down in background tabs. For a countdown, compute the remaining time from a fixed end timestamp on each tick.

8. useWindowSize

Returns the current window width and height and updates on resize.

import { useSyncExternalStore } from "react";

function subscribe(onChange: () => void) {
  window.addEventListener("resize", onChange);
  return () => window.removeEventListener("resize", onChange);
}

export function useWindowSize() {
  const width = useSyncExternalStore(subscribe, () => window.innerWidth, () => 0);
  const height = useSyncExternalStore(subscribe, () => window.innerHeight, () => 0);
  return { width, height };
}

When not to use it: For breakpoints: useMediaQuery only re-renders when the query flips, while this re-renders on every pixel of resizing. For the size of one element rather than the window, use a ResizeObserver. (The two separate snapshots are deliberate: returning a new object from one snapshot function would loop forever.)

9. useCopyToClipboard

Copies text with the Clipboard API and exposes a short-lived copied flag for button feedback.

import { useCallback, useState } from "react";

export function useCopyToClipboard(resetAfter = 1500) {
  const [copied, setCopied] = useState(false);
  const copy = useCallback(async (text: string) => {
    try {
      await navigator.clipboard.writeText(text);
      setCopied(true);
      window.setTimeout(() => setCopied(false), resetAfter);
      return true;
    } catch {
      setCopied(false); // insecure context or permission denied
      return false;
    }
  }, [resetAfter]);
  return [copied, copy] as const;
}

When not to use it: On pages served over plain HTTP: navigator.clipboard needs a secure context (HTTPS or localhost). Always show a fallback, such as selecting the text, when it returns false.

10. useFetch

Loads JSON for a URL, tracks loading and error state, and aborts the request when the URL changes or the component unmounts.

import { useEffect, useState } from "react";

type FetchState<T> = { data: T | null; error: Error | null; loading: boolean };

export function useFetch<T>(url: string | null): FetchState<T> {
  const [state, setState] = useState<FetchState<T>>({ data: null, error: null, loading: !!url });

  useEffect(() => {
    if (!url) return;
    const controller = new AbortController();
    setState((s) => ({ ...s, loading: true, error: null }));

    fetch(url, { signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error(`Request failed with status ${res.status}`);
        return res.json() as Promise<T>;
      })
      .then((data) => setState({ data, error: null, loading: false }))
      .catch((error: Error) => {
        if (error.name === "AbortError") return; // superseded or unmounted
        setState({ data: null, error, loading: false });
      });

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

  return state;
}

When not to use it: In larger apps. It has no cache, no deduplication, no retries and no revalidation, so two components asking for the same URL make two requests. Use your framework's data loading or a data library (TanStack Query, SWR) once you need any of that. React's own docs make the same recommendation for data fetching in effects.

11. useEventListener

Attaches an event listener to the window (or any element) and always calls the latest handler, without re-subscribing every render.

import { useEffect, useRef } from "react";

export function useEventListener<K extends keyof WindowEventMap>(
  type: K,
  handler: (event: WindowEventMap[K]) => void,
  target: Window | HTMLElement | null = typeof window === "undefined" ? null : window,
) {
  const saved = useRef(handler);
  useEffect(() => { saved.current = handler; }, [handler]);

  useEffect(() => {
    if (!target) return;
    const listener = (event: Event) => saved.current(event as WindowEventMap[K]);
    target.addEventListener(type, listener);
    return () => target.removeEventListener(type, listener);
  }, [type, target]);
}

// useEventListener("keydown", (e) => { if (e.key === "Escape") close(); });

When not to use it: For events React already handles on elements you render: use onClick, onKeyDown and friends directly on the JSX. Reach for this hook only for window, document or third-party elements.

12. useIsMounted

Returns a function that tells you whether the component is still on screen, for legacy async code that sets state after an await.

import { useCallback, useEffect, useRef } from "react";

export function useIsMounted() {
  const mounted = useRef(false);
  useEffect(() => {
    mounted.current = true;
    return () => { mounted.current = false; };
  }, []);
  return useCallback(() => mounted.current, []);
}

// const isMounted = useIsMounted();
// const data = await load(); if (isMounted()) setData(data);

When not to use it: As a default. React 18 removed the warning about setting state on unmounted components, and the better fix is cancelling the work itself (an AbortController, as in useFetch) or ignoring stale results with a flag inside the effect's cleanup. Keep this for older code you cannot restructure yet.

Which hook do I need? A quick decision list

Building a component library rather than individual hooks? Our React UI libraries comparison shows the same six components in shadcn/ui, Radix, Headless UI, daisyUI, Flowbite and MUI, and the Tailwind cheat sheet covers the utility classes used in the demos on this page. Ready-made React and Tailwind pieces are in the component gallery.

Frequently asked questions

How many built-in hooks does React have?

At the time of writing (October 2026, React 19.3), the React hooks reference lists 17 hooks in React itself: useState, useReducer, useContext, useRef, useImperativeHandle, useEffect, useLayoutEffect, useInsertionEffect, useEffectEvent, useMemo, useCallback, useTransition, useDeferredValue, useDebugValue, useId, useSyncExternalStore and useActionState. React 19 also adds useOptimistic and the use API, and React DOM adds useFormStatus.

What is the difference between useMemo and useCallback?

useMemo caches the result of calling a function, while useCallback caches the function itself. useCallback(fn, deps) is equivalent to useMemo(() => fn, deps). Use useMemo for expensive values and useCallback for functions passed to memoized children or used as effect dependencies.

Why does my useEffect run twice?

In development with Strict Mode on, React mounts each component, unmounts it and mounts it again to help you find effects that are missing cleanup. It does not happen in a production build. If running twice causes a bug, add a cleanup function that undoes what the effect started.

What does an empty dependency array mean in useEffect?

An empty array tells React the effect reads no props or state, so it runs once after the component mounts and its cleanup runs when the component unmounts. If the effect does read props or state, list them, otherwise it keeps using the values from the first render.

Can I use React 19 hooks like use or useActionState in React 18?

No. use, useActionState, useOptimistic and useFormStatus are not exported by React 18, and useEffectEvent needs React 19.2 or later. On React 18 you can get similar behavior with useState plus event handlers for forms, and with a ref that holds the latest callback instead of useEffectEvent.

What makes a function a custom hook?

Its name starts with use and it calls at least one other hook. That naming lets React's lint rules check it follows the rules of hooks. Each component that calls a custom hook gets its own separate state; hooks share logic, not data.

More in Guides

Related guides and tools

Start building

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap