Hooks
Hooks are functions that let a component hook into React features — state, lifecycle, context — from inside a plain function. useState and useEffect are the two you use constantly; the rest of the built-ins solve narrower problems, and custom hooks let you package any combination for reuse.
The rules of hooks
Section titled “The rules of hooks”There are exactly two, and both are enforced by eslint-plugin-react-hooks.
1. Only call hooks at the top level
Section titled “1. Only call hooks at the top level”Never inside conditions, loops, nested functions, or after an early return.
// All wrongif (isLoggedIn) { const [name, setName] = useState('');}for (const item of items) { useEffect(() => {}, []);}if (!user) return null;const [x, setX] = useState(0); // unreachable on some renders// Right: hooks first, unconditionally; put the condition insideconst [name, setName] = useState('');useEffect(() => { if (!isLoggedIn) return; …}, [isLoggedIn]);
if (!user) return null;Why. React does not know the names of your state variables. It stores hook data per component instance in an ordered list and walks that list in the same order on every render, matching call #1 to slot #1, call #2 to slot #2, and so on. There is no key beyond position.
Render 1: useState('') useState(0) useEffect(…) slot 0 slot 1 slot 2
Render 2 with a skipped hook: useState(0) useEffect(…) slot 0 ✗ slot 1 ✗ ← everything shifted; state is now wrongSkipping a hook on one render silently rebinds every subsequent hook to the wrong slot. Keeping calls unconditional keeps the order stable.
2. Only call hooks from React functions
Section titled “2. Only call hooks from React functions”Legal callers: a component function, or another hook. Not regular functions, not event handlers, not class methods, not callbacks.
// Wrongfunction handleClick() { const [x, setX] = useState(0); // no component is rendering}
// Rightfunction useSomething() { // a custom hook — name begins with "use" const [x, setX] = useState(0); return x;}The use prefix is not decoration: the linter uses it to decide whether a function is allowed to call hooks.
useRef
Section titled “useRef”useRef(initial) returns a mutable box, { current: initial }, that survives every render and does not trigger a re-render when changed.
Two distinct uses.
DOM refs
Section titled “DOM refs”Attach the ref to a JSX element and React sets ref.current to the DOM node after commit (and back to null on unmount).
import { useRef, useEffect } from 'react';
function SearchBox() { const inputRef = useRef(null);
useEffect(() => { inputRef.current.focus(); }, []);
return <input ref={inputRef} type="search" />;}This is the sanctioned way to do the small number of things that are genuinely imperative: focus, text selection, scrolling, measuring size, and driving media or canvas APIs.
inputRef.current.select();listRef.current.scrollIntoView({ behavior: 'smooth' });const { width, height } = boxRef.current.getBoundingClientRect();videoRef.current.play();Do not use refs to change what React renders (setting textContent, adding classes, hiding nodes). React will overwrite you on the next render, and you have reintroduced the imperative model React exists to remove.
A callback ref gives you the node as it attaches, which is useful when the element is conditional or in a list:
<div ref={(node) => { if (node) observer.observe(node); }} />Mutable values that must not cause renders
Section titled “Mutable values that must not cause renders”Anything the render output does not depend on: timer ids, previous values, instance counters, third-party objects, “did this already happen” flags.
const timeoutRef = useRef(null);
function startCountdown() { clearTimeout(timeoutRef.current); timeoutRef.current = setTimeout(onExpire, 5000);}
useEffect(() => () => clearTimeout(timeoutRef.current), []);The decision rule: if changing it should update the screen, it is state; if not, it is a ref.
Do not read or write a ref during rendering — that makes the render impure and its output depend on hidden history. Refs belong in effects and event handlers.
useMemo and useCallback
Section titled “useMemo and useCallback”Both cache something between renders, keyed by a dependency array with the same comparison rules as useEffect.
const value = useMemo(() => expensiveCompute(a, b), [a, b]); // caches a VALUEconst fn = useCallback(() => doSomething(a), [a]); // caches a FUNCTIONuseCallback(fn, deps) is exactly useMemo(() => fn, deps) with nicer syntax.
When they actually help
Section titled “When they actually help”Only two situations:
-
A genuinely expensive computation — sorting or filtering thousands of items, parsing, heavy formatting. “Expensive” means milliseconds you can measure in the Profiler, not a
.map()over ten rows.const sorted = useMemo(() => hugeList.slice().sort((a, b) => a.score - b.score),[hugeList]); -
Referential stability — keeping an object or function identity the same across renders, because something downstream compares by identity: a
memo()-wrapped child, auseEffectdependency, or anotheruseMemo.const handleSelect = useCallback((id) => setSelectedId(id), []);return <ExpensiveList items={items} onSelect={handleSelect} />; // ExpensiveList is memo()'d
Outside those cases they cost more than they save: extra allocations, a dependency array to keep correct, and noisier code. Wrapping a callback in useCallback while the child is not memoized accomplishes nothing at all.
useReducer
Section titled “useReducer”useReducer moves state transitions out of the component and into a pure function.
const [state, dispatch] = useReducer(reducer, initialState);reducer(state, action) returns the next state. dispatch(action) sends an action to it. Both dispatch and the reducer are stable — dispatch never changes identity, so it never needs to be a dependency.
import { useReducer } from 'react';
const initialState = { items: [], discount: 0 };
function cartReducer(state, action) { switch (action.type) { case 'added': return { ...state, items: [...state.items, action.item] }; case 'removed': return { ...state, items: state.items.filter((i) => i.id !== action.id) }; case 'quantity_changed': return { ...state, items: state.items.map((i) => i.id === action.id ? { ...i, quantity: action.quantity } : i ), }; case 'discount_applied': return { ...state, discount: action.percent }; case 'cleared': return initialState; default: throw new Error(`Unknown action: ${action.type}`); }}
function Cart() { const [state, dispatch] = useReducer(cartReducer, initialState);
return ( <> <button onClick={() => dispatch({ type: 'added', item: newItem })}>Add</button> <button onClick={() => dispatch({ type: 'cleared' })}>Clear</button> <p>{state.items.length} items</p> </> );}There is also a third argument for lazy initialization: useReducer(reducer, initialArg, init) calls init(initialArg) once to produce the initial state.
Prefer it over useState when
Section titled “Prefer it over useState when”- Several state values change together in fixed combinations (loading + data + error).
- The same update logic is triggered from many handlers.
- The next state depends on the current state in non-trivial ways.
- You have “state machine” behaviour — a set of legal transitions you want in one readable place.
- You want to test transitions: a reducer is a pure function, testable without React.
Stick with useState for independent, simple values. A boolean toggle does not need a reducer.
The reducer must be pure: no fetching, no mutation of state, no Math.random(). Under Strict Mode React calls reducers twice in development to catch impurity.
useContext
Section titled “useContext”Context passes a value to every component below a provider, without threading props through the layers in between (prop drilling).
Three steps.
import { createContext } from 'react';
export const ThemeContext = createContext('light'); // argument is the defaultimport { useState } from 'react';import { ThemeContext } from './ThemeContext';
function App() { const [theme, setTheme] = useState('dark');
return ( <ThemeContext.Provider value={theme}> <Layout /> </ThemeContext.Provider> );}import { useContext } from 'react';import { ThemeContext } from './ThemeContext';
function Button({ children }) { const theme = useContext(ThemeContext); return <button className={`btn-${theme}`}>{children}</button>;}useContext reads the value from the nearest matching provider above in the tree. The default passed to createContext is used only when there is no provider at all — which usually means a bug, so a common idiom is a null default plus a guard hook:
const AuthContext = createContext(null);
export function useAuth() { const ctx = useContext(AuthContext); if (ctx === null) throw new Error('useAuth must be used inside <AuthProvider>'); return ctx;}Providers nest: an inner provider overrides an outer one for its subtree. That is how per-section themes work.
The re-render cost
Section titled “The re-render cost”Every consumer re-renders when the provider’s value changes, identity-compared. Passing an object literal means a new value on every render of the provider, so every consumer re-renders every time:
// New object each render → all consumers re-render<AuthContext.Provider value={{ user, login, logout }}>Memoize the value, and keep the functions stable:
const login = useCallback((creds) => { … }, []);const logout = useCallback(() => { … }, []);const value = useMemo(() => ({ user, login, logout }), [user, login, logout]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;Also split contexts that change at different rates — a rapidly-changing value and a stable set of actions should be two contexts, so consumers of the actions do not re-render on every change of the value.
Custom hooks
Section titled “Custom hooks”A custom hook is a function whose name starts with use and which calls other hooks. It exists to reuse stateful logic, not UI.
Crucially, each call gets its own independent state — the hook is a recipe, not a shared instance.
import { useState, useCallback } from 'react';
export function useToggle(initial = false) { const [on, setOn] = useState(initial); const toggle = useCallback(() => setOn((v) => !v), []); return [on, toggle, setOn];}const [isOpen, toggleOpen] = useToggle();const [isMuted, toggleMuted] = useToggle(true); // completely separate stateSomething with cleanup:
import { useEffect, useRef } from 'react';
export function useEventListener(target, type, handler) { const savedHandler = useRef(handler);
useEffect(() => { savedHandler.current = handler; // always call the latest version }, [handler]);
useEffect(() => { const el = target ?? window; const listener = (e) => savedHandler.current(e); el.addEventListener(type, listener); return () => el.removeEventListener(type, listener); }, [target, type]);}The ref indirection is a common custom-hook idiom: it lets the effect stay attached (deps unchanged) while still calling the newest callback, avoiding a resubscribe on every render.
A debounced value:
import { useState, useEffect } from 'react';
export function useDebouncedValue(value, delay = 300) { const [debounced, setDebounced] = useState(value);
useEffect(() => { const id = setTimeout(() => setDebounced(value), delay); return () => clearTimeout(id); // a new keystroke cancels the pending update }, [value, delay]);
return debounced;}const [query, setQuery] = useState('');const debouncedQuery = useDebouncedValue(query, 400);
useEffect(() => { if (debouncedQuery) search(debouncedQuery);}, [debouncedQuery]);Guidelines
Section titled “Guidelines”- Name it
use…— required for the lint rules to work. - Return whatever fits: an array when the names are up to the caller (like
useState), an object when there are several named things. - Keep it focused. A hook that does five unrelated things is a module, not a hook.
- Extract logic, not just lines. A custom hook wrapping a single
useStateearns nothing. - Hooks compose: custom hooks calling custom hooks is normal and is how libraries are built.
The rest of the built-ins
Section titled “The rest of the built-ins”You will meet these less often; know they exist and what problem they solve.
| Hook | Purpose |
|---|---|
useLayoutEffect |
Like useEffect but synchronous before paint — for measuring and adjusting layout without flicker. |
useId |
Generates a stable unique id, consistent between server and client. For htmlFor/aria-describedby, not for list keys. |
useTransition |
Marks a state update as non-urgent so typing stays responsive; gives you an isPending flag. |
useDeferredValue |
Renders a lagging copy of a value, so expensive subtrees don’t block input. |
useSyncExternalStore |
Subscribes safely to a store outside React. What state libraries use internally. |
useImperativeHandle |
Customizes what a parent’s ref exposes — a narrow API instead of the raw DOM node. |
useDebugValue |
Labels a custom hook in React DevTools. |
React 19 adds hooks aimed at forms and async work: use (read a promise or context, and it may be called conditionally), useActionState, useOptimistic, and useFormStatus from react-dom. They are most useful with a framework that supports server actions.
Key points
Section titled “Key points”- Hooks must be called unconditionally at the top level, because React matches them to storage by call order.
- Hooks may only be called from components or other hooks; the
useprefix is what makes a function a hook. useRefis a render-surviving box that does not cause re-renders — for DOM nodes and for values the UI doesn’t display.useMemo/useCallbackpay off only for measurably expensive work or for referential stability that something downstream depends on.useReducercentralizes complex or multi-value transitions in a pure, testable function;dispatchis stable.useContextremoves prop drilling but re-renders every consumer when the provider value’s identity changes — memoize it and split contexts by change rate.- Custom hooks reuse stateful logic; every call site gets its own state.