React's useReducer returns a dispatch function. Is that function's identity stable across re-renders, and what does the guarantee let you do?
answer
- same reference on every render
- nothing for a deps array to notice
- memoized children keep their memo
- two contexts, one of them never changes
- the reducer gets no such promise
basics
~20 sYes. React guarantees the dispatch function returned by useReducer keeps the same identity for the component's whole lifetime, so it can be omitted from dependency arrays, passed to memoized children, and put in context without causing churn.
solid answer
~40 sReact guarantees `dispatch` has a stable identity: the same function object is returned on every render of that component instance, so `Object.is` comparisons against it never fail. That has three practical payoffs. First, you can omit it from a `useEffect` or `useCallback` dependency array — `react-hooks/exhaustive-deps` knows it is stable and will not ask for it. Second, passing `dispatch` as a prop to a `React.memo` child never breaks the child's memoization, unlike an inline arrow. Third, you can put `dispatch` in its own context and consumers of that context will not re-render when the state changes. The reducer function itself is not stable if you define it in the component body — but that is fine, because React does not compare reducers.
go deeper
Remember the headline: dispatch never changes identity, so you do not need to list it in a dependency array or wrap it in useCallback.
Explain what stability means mechanically — the same object under Object.is on every render — and name the concrete places it matters: dependency arrays, memoized child props, and context values.
Demonstrate that you use the guarantee deliberately: splitting state and dispatch contexts so writer components do not re-render, and converting stale-closure interval effects into dispatch-only effects with empty deps.
Be ready to weigh the coupling cost of passing raw dispatch across a boundary — children then know your action vocabulary — against the memoization it preserves, and to say when a bound callback is the better API despite the identity churn.
## What "stable identity" means Every render of a function component re-executes the whole body, so any function literal written there is a brand-new object each time. React deliberately exempts a few values from that: the `dispatch` returned by `useReducer`, the setter returned by `useState`, and the object returned by `useRef`. For a given mounted component, React hands back the identical `dispatch` reference on every render. Two renders' worth of `dispatch` compare equal under `Object.is`. This is a documented guarantee, not an implementation accident, which is what makes it safe to build on. ## Payoff 1: dependency arrays A dependency array is compared entry by entry with `Object.is`. An unstable function in it makes the effect re-run every render. Since `dispatch` cannot change, you may leave it out entirely: ```js useEffect(() => { const id = setInterval(() => dispatch({ type: 'tick' }), 1000); return () => clearInterval(id); }, []); ``` The `react-hooks/exhaustive-deps` lint rule recognises `dispatch` as a known-stable value and does not flag its absence. Including it is harmless but noise. This is the reason the "interval that reads stale state" bug often disappears when you move from `useState` to `useReducer`: an action carries no captured state, so the effect needs no state in its deps and never needs to be torn down and re-created. ## Payoff 2: memoized children `React.memo` skips re-rendering a component when its props are shallow-equal to the previous ones. An inline handler defeats that instantly: ```jsx <Row onDelete={() => dispatch({ type: 'delete', id })} /> // new function every render <Row dispatch={dispatch} id={id} /> // stable prop ``` The first line hands `Row` a fresh function object each render, so the memo comparison always fails. The second passes the stable `dispatch` and lets `Row` build the action itself, so the memo holds. Note the tradeoff: pushing action construction into the child couples the child to your action vocabulary. It is a legitimate technique, not a default. ## Payoff 3: splitting state and dispatch in context Context consumers re-render whenever the provider's value changes identity. If state and dispatch travel in one object, every consumer re-renders on every state change — including components that only ever dispatch. Because `dispatch` is stable, you can hand it out through its own context whose value never changes, so pure-writer components subscribe to nothing that moves. ## Why the reducer's own identity does not matter A reducer defined inside the component body is a new function every render, and people sometimes reach for `useCallback` on it. That is unnecessary. React does not compare the reducer against the previous one, does not re-initialise state when it changes, and does not include it in any dependency comparison of its own. Defining the reducer at module scope is still preferable — it makes the purity obvious and the unit test trivial — but a component-scoped reducer is not a correctness or performance bug. ## What stability does not buy you Stable `dispatch` is not a re-render optimisation on its own. The component that owns the reducer still re-renders whenever the state changes, and so do its children unless they are memoized or composed out of the way. Nor does stability say anything about ordering or timing: dispatching still queues an action for the next render. One more thing it does not mean: the reducer that eventually processes your action is the one React has at the time the update is processed, not a snapshot taken when `useReducer` was first called. So a reducer that closes over a prop still sees fresh values — but a reducer that closes over anything is already violating purity, and you should pass that value in the action instead. ## How to answer this in an interview Say "guaranteed stable" first, then name the three consequences — deps, memo props, context split — and finish by noting that the reducer itself is not stable and does not need to be. That progression shows you know the guarantee, why it exists, and where it stops.
- If dispatch is stable, why do you still see the whole subtree re-render after every action?Because stability only concerns the function's identity, not React's render scheduling. The component owning the state re-renders on every accepted action, and children re-render with it unless they are wrapped in `React.memo` or passed down as `children` so they are not re-created. Stable `dispatch` prevents one specific cause of extra renders; it does not prevent renders.
- Should you wrap a reducer defined inside the component in useCallback?No. React never compares reducers between renders, does not re-initialise state when the reducer's identity changes, and puts it in no dependency comparison, so memoizing it buys nothing. The better move is to lift the reducer to module scope — that makes its purity explicit and lets you unit-test it without rendering.
- How does the stability guarantee help an interval that used to read stale state?An interval effect that reads state must list that state as a dependency, so it is torn down and re-created on every change. Dispatching an action instead needs no state in the closure: the effect can list only `dispatch`, which never changes, so the interval is created once and the reducer always computes from the current state.
saying these in an interview costs you the question
- Wraps dispatch in useCallback to make it stable
- Claims exhaustive-deps demands dispatch in the array
- Says a stable dispatch stops the component re-rendering
- Assumes the reducer is memoized because dispatch is
- Puts state and dispatch in one context value and expects no extra renders