Call hooks only at the top level of a React function component or a custom hook, never inside conditions, loops, nested functions or after an early return. React identifies each hook by the order of calls, so that order must be the same on every render.
Top level only: an if around useState shifts every later hook by one slot on the render where the condition flips, and the component reads someone else's state.
Only from React functions: a hook called from a plain helper or a class has no component that owns its state.
Custom hooks count: a function whose name starts with use may call hooks, and the same rules apply inside it.
**use is the exception**: the React 19 use API may be called inside if and loops, though still not inside try/catch.
Enforcement: eslint-plugin-react-hooks (rules-of-hooks) catches violations at lint time; at runtime React throws errors like "Rendered more hooks than during the previous render".
ReactMiddle
Hooks in Depth
Rules of hooks, custom hooks, useReducer, useContext, useMemo and useCallback, useId, useSyncExternalStore, useTransition, use
Questions closed
0 / 19
- 0Review
- 9Asked often
Questions
A custom hook is a function whose name starts with use and that calls other hooks. It shares stateful logic between components, not state: each component that calls it gets its own copy of that state.
Extract when: two components repeat the same useState + useEffect pair, or a component mixes UI with a block of logic that has a clear name (useDebouncedValue, useOnlineStatus, useMediaQuery).
**The use prefix matters: it tells the linter and the React Compiler to check the rules of hooks inside the function.
Do not extract a function that calls no hooks; a plain function is simpler and can be called anywhere, even conditionally.
Name it by what it does**, not by its lifecycle: useChatConnection(roomId) rather than useMountEffect.
useContext lets any component read a value from the nearest provider above it without passing the value through every intermediate component as props (prop drilling). You create a context with createContext(defaultValue), wrap a subtree in a provider and call useContext(MyContext) below.
React 19: the context object itself is the provider, <ThemeContext value={theme}>; <ThemeContext.Provider> still works but is on its way out.
Nearest provider wins: nested providers override the value for their subtree.
Default value is used only when there is no provider above at all, not when the provider passes undefined.
Good fits: theme, locale, the signed-in user, a feature's state and dispatch. Data that changes many times per second is a poor fit, because every consumer re-renders on each change.
Use useReducer when the next state depends on the previous one through several kinds of updates, or when several fields change together: all the transitions live in one pure reducer(state, action) function instead of being spread across event handlers.
Signals to switch: many setX calls in one handler, fields that must stay consistent (status, data, error), or the same update logic copied into several handlers.
Testable: the reducer is a plain function, so you test transitions without rendering anything.
**Stable dispatch: its identity never changes, so it is safe to pass down or put into context without memoization.
Pure**: a reducer must not mutate state, fetch or read the clock; in StrictMode React calls it twice in development to expose that.
For one or two independent values, useState is shorter and clearer.
React.memo wraps a component and skips re-rendering it when its props equal the previous ones. useMemo is a hook called inside a component that caches a computed value between renders until one of its dependencies changes.
Level: memo applies to a whole component, once, where it is defined; useMemo applies to one value inside a render.
Comparison: memo compares each prop with Object.is; useMemo compares the items of its dependency array the same way.
They work together: a memo child skips its render only if every prop keeps its reference, and useMemo (or useCallback for a function) is how the parent keeps an object or array prop stable.
React Compiler applies the equivalent of both automatically, so a compiled codebase rarely needs either by hand.
By call order. Each component instance (its fiber) keeps a list of hook slots; on every render React walks that list in step with the hook calls, so the first useState reads slot 1, the second reads slot 2, and so on.
First render: each hook call appends a new slot holding its state, its queue of pending updates or its effect.
Later renders: each call takes the next slot; React does not look at variable names or arguments.
Why the rules exist: if a condition skips a hook, every later call reads the wrong slot. When the count differs React throws "Rendered more hooks than during the previous render"; when only the kinds differ, it warns about a change in the order of hooks.
Custom hooks are transparent: their inner hooks are just more slots in the calling component's list, which is why two components using one custom hook never share its state.
State lives with the instance: unmount the component or change its key and the list, with all its state, is thrown away.
useMemo(fn, deps) caches the result of calling fn; useCallback(fn, deps) caches the function itself, and is the same as useMemo(() => fn, deps). Both return the previous value until a dependency changes by Object.is.
Worth it for: a calculation that is measurably slow (sorting or filtering thousands of rows); a value or callback passed to a memo child, which skips re-rendering only if its props are referentially equal; a value used in another hook's dependency array.
Not worth it for: cheap expressions or callbacks passed to plain DOM elements; the comparison and the closure cost more than they save.
A missing dependency returns a stale value or a callback that sees old state; trust the exhaustive-deps lint rule.
Cache, not a guarantee: React may drop a cached value (for example, during development hot reload), so code must stay correct without it.
React Compiler 1.0 memoizes components and values automatically, so in a compiled codebase new code rarely needs either hook; they remain an escape hatch, for example to keep an effect dependency stable.
When a provider receives a new value (compared with Object.is), React re-renders every component that reads that context, even if it uses only one field, and memo on the consumer does not stop it. A provider value created inline is a new object on every parent render.
Memoize the value: wrap the object in useMemo (or rely on the React Compiler) so it changes only when its contents change.
Split contexts: put rarely changing parts (dispatch, the API client) and frequently changing state in separate contexts; components that only dispatch then never re-render on state changes.
Split by domain: ThemeContext, AuthContext, CartContext instead of one AppContext.
Move state down: keep fast-changing state (input text, hover) local instead of in context.
Need selectors? Context has none. A store with useSyncExternalStore (Zustand, Redux) re-renders only the components whose selected slice changed.
The early return sits above useState and useEffect, so a guest render calls no hooks after it and a signed-in render calls two more. React matches hooks by call order and throws when the count changes between renders.
On the first render user is null, the component returns before reaching the hooks, and React records zero of them after useContext.
After sign-in the same instance renders with a user, reaches useState, and React finds more hooks than it stored.
Fix: call every hook unconditionally first and put the early return after them; the effect itself checks user.
Another fix is to split the signed-in part into its own component (<ProfileDetails user={user} />), which mounts with its own hooks.
eslint-plugin-react-hooks reports this exact pattern as "React Hook is called conditionally".
It logs 0. useCallback with an empty dependency array keeps the function from the first render, and that function closed over count as it was then, so it never sees later values (a stale closure).
Each render has its own count constant; the cached log belongs to render one.
Fix: list the dependency, useCallback(() => console.log(count), [count]), as exhaustive-deps would demand.
If the callback must keep a stable identity and still read the latest value, store the value in a ref, or, inside an effect, use useEffectEvent (stable since React 19.2).
With the React Compiler you would drop useCallback here entirely; the compiler tracks count as a dependency itself.
Keep the state in a useReducer inside a provider component, expose state and dispatch through two contexts, and wrap the reads in small custom hooks. Any component in the subtree then reads state or dispatches actions without prop drilling.
Custom hooks as the API: useCart() and useCartDispatch() hide the contexts and throw a clear error when called outside the provider.
Two contexts: dispatch never changes, so components that only send actions do not re-render on state changes.
Where it stops scaling: every reader of the state context re-renders on any change, because context has no selectors; large, frequently updated state (a big table, live data) gets slow.
No built-in tooling: no devtools time travel, middleware or persistence; you write them yourself.
Then reach for Zustand, Redux Toolkit or a similar store, which subscribes components to slices; server data belongs in TanStack Query, not in a reducer.
useId() returns a unique string id that is stable for the lifetime of the component and identical on the server and the client, so it is safe for linking a <label> to an <input> or an aria-describedby to a hint in server-rendered HTML.
**Math.random() gives different ids on the server and on the client, which causes a hydration mismatch, and a new id on every render.
A module counter** depends on render order; the server and the client, or two concurrent requests on the server, can count differently.
useId derives the id from the component's position in the tree, which is the same in both environments.
One call, many ids: append suffixes, ${id}-email, ${id}-hint.
Not for list keys: keys must come from your data. Several React roots on one page need distinct identifierPrefix options.
Both mark an update as non-urgent so React can render it in the background and interrupt it for urgent input like typing. useTransition wraps the state update you control; useDeferredValue gives you a lagging copy of a value you only receive.
**useTransition**: returns [isPending, startTransition]. Wrap setTab(next) in startTransition and the old tab stays on screen and responsive while the new one renders; isPending drives a spinner or a dimmed state.
React 19: startTransition accepts an async function (an Action), and isPending stays true until it settles.
**useDeferredValue(value)**: re-renders first with the old value, then with the new one in the background. Use it when the slow part receives the value as a prop, for example a heavy list filtered by query.
Never for controlled inputs: the input's own state must update urgently, or typing lags; defer the expensive consumer instead.
With Suspense: inside a transition React keeps showing old content instead of falling back to a loader.
They make slow renders interruptible, not faster; the heavy child must be wrapped in memo (or compiled) to benefit.
use(resource) reads a promise or a context during render. With a promise, the component suspends until it resolves (the nearest <Suspense> shows its fallback, a rejection goes to the nearest error boundary); with a context, it works like useContext.
Conditional calls are allowed: unlike other hooks, use may sit inside if and loops, so you can read a context only on the branch that needs it. It still may not be called inside try/catch.
The promise must be stable: a promise created during render is new on every render, so the component suspends again on each attempt and React warns about an uncached promise. Create it in a Server Component and pass it down, or take it from a cache (a framework loader, a data library).
Typical use: a Server Component starts a fetch without awaiting and passes the promise to a Client Component, which calls use(promise) inside a Suspense boundary.
Not a fetching tool on its own: it has no caching, retries or invalidation.
Restart a timer on every change and act only when it fires after a pause. The usual shape is a debounced value: setTimeout in useEffect, cleared in the effect's cleanup. For work that is not state, such as a request or an autosave, debounce a callback and keep its timer in a ref.
Debounced value: useDebouncedValue(query, 300) returns a copy of query that updates 300 ms after typing stops; each keystroke re-runs the effect, whose cleanup cancels the previous timer. Feed the result into the query key.
Debounced callback: the returned function keeps one identity across renders, so the timer lives in useRef, not in a local variable.
The classic bug: const search = debounce(onSearch, 300) (lodash) in the component body creates a new debounced function, with its own timer, on every render, so every keystroke still sends a request.
Latest callback: keep fn in a ref updated after each render, or the stable function keeps calling the first render's fn with stale props.
Cleanup: clear the pending timer on unmount, or the request goes out after the user has left the page.
Treat localStorage as an external store and read it with useSyncExternalStore: subscribe to the storage event for writes from other tabs, fire your own event for writes in this tab, and return null from the server snapshot so server rendering never touches window.
Cross-tab sync: the browser fires storage in every other tab of the same origin when a key changes, never in the tab that wrote it; the custom event keeps two components in one tab in sync.
SSR and hydration: the server snapshot renders the initial value on the server and during hydration, then React re-renders with the stored value, with no hydration mismatch.
Stable snapshot: return the raw string, equal by value until the data changes; JSON.parse inside getSnapshot returns a new object on every call and loops forever, so parse in useMemo.
Bad data: JSON.parse sits in try/catch, because a value from an older release or edited by hand must not crash the page; setItem also throws when the quota is full or storage is blocked.
**Why not useState + useEffect**: a lazy initializer that reads localStorage crashes on the server ("localStorage is not defined"), and a copy in state never sees writes from other tabs or other components.
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?) subscribes a component to a store that lives outside React (a browser API, a module-level store) and reads it in a way that is safe under concurrent rendering. It is what Zustand, Redux and similar libraries use internally.
Why not useState + useEffect
Tearing: during an interruptible render the store can change between rendering two components, and the screen shows two different versions of the same data. useSyncExternalStore detects a changed snapshot and re-renders synchronously, so everything stays consistent.
Missed updates: a store change between render and the effect's subscription is lost unless you re-read it by hand.
SSR: getServerSnapshot supplies the value for server rendering and hydration.
The contract
subscribe(callback) registers a listener and returns an unsubscribe function; define it outside the component, or React resubscribes on every render.
getSnapshot() must return the same value (by Object.is) while the store has not changed; returning a new object each time causes an infinite loop.
Updates from an external store are synchronous; they cannot be marked as transitions.
getSnapshot builds a new object on every call, so React sees a different snapshot each time it checks, decides the store changed, re-renders, and checks again forever. The snapshot must be the same reference until the store really changes.
React calls getSnapshot on every render and again before committing to detect tearing; it compares results with Object.is.
Fix 1: return a value the store already holds (cartStore.getState() returns the same state object until an update replaces it) and derive from it in render.
Fix 2: select a primitive, such as () => cartStore.getState().items.length, which is equal by value.
Fix 3: if a derived object is needed, memoize it against the source reference, which is what selector libraries (reselect, Zustand's useShallow) do.
subscribe is defined once in the module, which is already correct; an inline subscribe would resubscribe on every render.
Decide what the hook owns, return the smallest useful surface in a stable shape, and keep everything it returns referentially stable unless its meaning changed. A good hook reads like a built-in: one clear job, predictable inputs, no hidden effects.
Shape of inputs and outputs
Return a tuple ([value, setValue]) for a useState-like pair the caller will rename; return an object when there are three or more fields or the caller picks a few.
Options object for anything beyond one or two arguments: useFetch(url, { enabled, retry }) stays readable and extends without breaking callers.
Accept values, not setters of the caller, and let the caller decide what to do with the result.
Stability
Functions it returns should keep their identity (useCallback, a useReducer dispatch, or the React Compiler), so callers can pass them to memo children and dependency arrays.
Do not create a new object on every render if nothing changed; memoize the returned object or return primitives.
A callback passed in by the caller (onChange) should not restart subscriptions every render: read the latest one from a ref or useEffectEvent.
Boundaries
Do not hide data fetching with caching needs in a homemade hook; wrap TanStack Query instead.
Keep it free of UI: a hook that returns JSX is a component in disguise.