What does the third argument in React's useReducer(reducer, initialArg, init) do, and when is passing it worth it?
answer
- the argument you pass uncalled
- mount only, never again
- the second argument is evaluated every render
- one function for start and for reset
- not re-run when the input prop changes
basics
~20 sThe third argument is a lazy initializer: React sets the initial state to init(initialArg) instead of initialArg, calling it only when the component mounts. It avoids rebuilding expensive initial state on every render and gives you one named function that also serves a reset.
solid answer
~40 sWhen you pass `init`, React computes the initial state as `init(initialArg)` rather than using `initialArg` directly, and it calls `init` only on the initial mount — not on subsequent renders. That matters because the second argument is an ordinary expression: if you write `useReducer(reducer, buildInitialState(items))`, `buildInitialState` runs on every single render and its result is thrown away after the first. Moving it into the third slot means the work happens once. The second, arguably bigger win is that you now have a named, testable function that produces a valid starting state, so a `reset` action in the reducer can call the same `init` and you cannot drift between "how state starts" and "how state resets".
code
javascript · 17 linesfunction createInitialState(userId) {
return { userId, items: [], status: 'idle' };
}
function reducer(state, action) {
switch (action.type) {
case 'loaded':
return { ...state, items: action.items, status: 'ready' };
case 'reset':
return createInitialState(action.userId);
default:
throw new Error('Unknown action: ' + action.type);
}
}
console.log(createInitialState('u1'));
console.log(reducer(createInitialState('u1'), { type: 'loaded', items: [1, 2] }));go deeper
Know that a third argument makes the initial state init(initialArg) and that React calls it only when the component mounts. Pass the function without calling it.
Explain why the two-argument form can waste work: the second argument is an expression re-evaluated on every render even though React uses only the first result. Say that init never re-runs when its input changes.
Show judgment about when it earns its place — real initialization cost, or one shared definition of a valid starting state reused by a reset action — and reach for a key change rather than an effect when state must restart from a new prop.
Be ready to argue where initialization belongs at all: derived from props on the fly, produced by an initializer, or owned outside the component. Frame the reset-versus-remount choice as a component-identity decision, not a hook detail.
## The mechanic ```js const [state, dispatch] = useReducer(reducer, initialArg, init); ``` With two arguments, the initial state **is** `initialArg`. With three, the initial state is `init(initialArg)` — React calls your initializer with the second argument and stores the result. Either way, the initialization happens exactly once, on mount, and is ignored on every later render. ```js function createInitialState(userId) { return { userId, items: [], selected: new Set(), status: 'idle' }; } const [state, dispatch] = useReducer(reducer, userId, createInitialState); ``` ## Why the plain form can be wasteful The distinction people miss is that the second argument is an ordinary JavaScript expression evaluated on every render, because the whole component body re-runs. React discards the value after the first render, but it does not stop you from computing it: ```js // createInitialState(userId) runs on EVERY render; only the first result is used const [state, dispatch] = useReducer(reducer, createInitialState(userId)); ``` If the function parses a payload, hydrates from `localStorage`, builds a `Map`, or walks a large array, you are paying that on every keystroke. Passing the function itself instead of its result moves the cost to mount only. Note the shape difference precisely: the third argument is the function **uncalled**. `useReducer(reducer, userId, createInitialState())` is a bug — it invokes immediately and hands React whatever came back, usually not a function at all. Be honest about the size of the win, though. For a small object literal like `{ count: 0 }` there is nothing to save, and adding an initializer is pure ceremony. This is an optimisation for genuinely expensive initialization, and interviewers do listen for whether you know that. ## The reuse win The more durable reason to use `init` has nothing to do with speed. Once initial state is produced by a named function, that function becomes the single definition of "a valid starting state", and the reducer can reuse it: ```js function reducer(state, action) { if (action.type === 'reset') return createInitialState(action.userId); // ... } ``` Without it, mount-time state and reset-time state are two separate literals that quietly diverge the first time someone adds a field to one of them. It also gives you something you can unit-test on its own: assert that `createInitialState(id)` produces the invariants your reducer relies on. ## What init does not do - **It does not re-run when `initialArg` changes.** If `userId` becomes a different value on a later render, React does nothing — state is already initialized. Re-initializing on a prop change is a separate problem, and the idiomatic React answer is to give the component a `key` so it remounts, not to watch the prop in an effect. - **It does not run on every render.** That is the entire point. - **It is not required to be pure in the reducer sense**, but it should be: React may call it during a render that is later discarded, and reading or writing anything outside its arguments makes that visible. - **It receives exactly one argument** — whatever you passed as `initialArg`. If the initializer needs several inputs, pass an object. ## Relationship to the useState initializer `useState` has the same idea in a different shape: `useState(() => expensiveThing())` passes a zero-argument function that React calls once. `useReducer` splits it into a value plus a function that transforms it, which is what makes the function reusable for a reset — the `useState` form closes over its inputs instead of receiving them. ## Interview framing The strongest answer covers three beats: what it computes (`init(initialArg)`), when it runs (mount only, so it avoids per-render work that the two-argument form would repeat), and why you would still reach for it when nothing is expensive (one named source of truth for a valid starting state, reusable by a reset action and testable on its own).
- If initialArg changes on a later render, does React re-run init?No. Initialization happens once on mount; afterwards React ignores both arguments entirely. If a component genuinely must restart from a new input, the idiomatic answer is to change its `key` so React unmounts and remounts it with fresh state, rather than watching the prop in an effect and dispatching a reset.
- What breaks if you write useReducer(reducer, userId, createInitialState()) with the parentheses?You call the initializer yourself and hand React its return value as the third argument. React will then try to call that value as a function during initialization, which throws if it is an object. The third argument must be the function itself, uncalled — React supplies the argument.
- Is the lazy initializer worth adding for a small object like { count: 0, error: null }?No. Building that literal costs nothing measurable, and the extra indirection makes the component harder to read. Reach for the third argument when initialization is genuinely expensive — parsing, hydrating from storage, building maps over a large array — or when you want one named function shared between mount and a reset action.
saying these in an interview costs you the question
- Thinks init re-runs when initialArg changes
- Passes init already invoked, with parentheses
- Believes the two-argument initial value is computed only once
- Adds a lazy initializer to a trivial object literal for 'performance'
- Confuses it with useState's zero-argument initializer signature