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.
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.
| Hook | Group | Use it to | Since |
|---|---|---|---|
| useState | State | hold a value that re-renders the component when it changes | React 18 |
| useReducer | State | manage state with many related updates through one reducer function | React 18 |
| useContext | Context | read a value from the nearest provider above | React 18 |
| useRef | Ref | keep a mutable value or a DOM node that does not trigger renders | React 18 |
| useImperativeHandle | Ref | choose which methods a parent can call through a ref | React 18 |
| useEffect | Effect | sync with something outside React after paint | React 18 |
| useLayoutEffect | Effect | measure layout before the browser paints | React 18 |
| useInsertionEffect | Effect | insert style rules before layout effects run (CSS-in-JS libraries) | React 18 |
| useEffectEvent | Effect | read the latest props inside an effect without re-running it | React 19.2 |
| useMemo | Performance | cache an expensive calculation between renders | React 18 |
| useCallback | Performance | cache a function so memoized children do not re-render | React 18 |
| useTransition | Performance | mark a state update as non-urgent and get a pending flag | React 18 |
| useDeferredValue | Performance | let a slow part of the UI lag behind fast typing | React 18 |
| useId | Other | generate stable unique IDs for labels and ARIA attributes | React 18 |
| useSyncExternalStore | Other | subscribe to a store that lives outside React | React 18 |
| useDebugValue | Other | label a custom hook in React DevTools | React 18 |
| useActionState | Other | track the result and pending state of a form action | React 19 |
| useOptimistic | Other | show the expected result of an action before it finishes | React 19 |
| use | API | read a promise or context, even inside conditions | React 19 |
| useFormStatus | React DOM | read the pending state of the parent form | React 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.
useStateWhat 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" }).
useReducerWhat 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.
useContextWhat 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.
useRefWhat 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.
useImperativeHandleWhat 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.
useEffectWhat 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.
useLayoutEffectWhat 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.
useInsertionEffectWhat 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.
useEffectEventWhat 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.
useMemoWhat 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.
useCallbackWhat 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.
useTransitionWhat 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.
useDeferredValueWhat 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).
useIdWhat 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.
useSyncExternalStoreWhat 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.
useDebugValueWhat 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()).
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.useActionStateWhat 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.
useOptimisticWhat 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.
useWhat 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.
useFormStatusWhat 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.
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 write | The effect runs | Typical use |
|---|---|---|
| useEffect(fn) | after every render | rare; usually a sign something should be computed during render |
| useEffect(fn, []) | once after mount (twice in development Strict Mode), cleanup on unmount | subscriptions and setup that use no props or state |
| useEffect(fn, [a, b]) | after mount, then whenever a or b changes | fetching 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.
eslint-plugin-react-hooks and let exhaustive-deps tell you what is missing.{ 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.[user.id] is more stable than [user] when only the ID matters.setCount(c => c + 1) does not read count, so count does not need to be listed.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]);
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.
useDebounceReturns 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.
useLocalStorageWorks 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.
useMediaQueryReturns 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.
useToggleA 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.
usePreviousReturns 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.
useOnClickOutsideCalls 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.
useIntervalA 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.
useWindowSizeReturns 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.)
useCopyToClipboardCopies 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.
useFetchLoads 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.
useEventListenerAttaches 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.
useIsMountedReturns 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.
useState (or useReducer if several values change together).useRef.useContext, or an external store read with useSyncExternalStore.useEffect; switch to useLayoutEffect only for measurements that cause a visible flicker.useMemo. A memoized child keeps re-rendering because of a function prop: useCallback.useDeferredValue or useTransition.useActionState, useFormStatus, useOptimistic.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.
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.
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.
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.
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.
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.
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.
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.