“Why does my item get added twice in development but only once in production?” Interviewers like this question because it sounds like a React bug, and the candidate who can explain that it is their own impure updater function, exposed on purpose by Strict Mode, clearly understands the contract React expects from state updates.
A close cousin of the question is “when should you use setX(prev => ...) instead of setX(value), and what are the rules for that function?” Knowing when to use the functional form is half the answer. Knowing what the function is allowed to do is the half most candidates miss.
Before you start
You should know how useState works and that setting state queues a re-render rather than changing the variable in place (the state snapshot guide covers this). You should also know the difference between mutating an array with push and creating a new one with spread syntax. The examples run in React 18 or 19; Strict Mode double invocation exists in both.
The short answer
A functional update, setState(prev => next), hands React a function to run while it processes the state queue. React treats that function like a reducer: it must compute the next state only from its argument and must not mutate the argument or cause side effects. In development under Strict Mode, React calls it twice and keeps one result, so a mutation or a network call inside it shows up as duplicated data. Keep updaters as pure calculations and put side effects in the event handler.
How it works
React does not call your updater at the moment you call the setter. It stores the function in the hook’s queue and runs it during the next render, when it computes the new state. Because React may run that computation more than once (Strict Mode in development, or when concurrent rendering discards and restarts work), the function must give the same answer for the same input and leave the input untouched.
Here is an impure updater and what double invocation does to it:
const prev = { items: ['a'] };
const impure = p => { p.items.push('b'); return { ...p }; };
impure(prev);
const result = impure(prev);
console.log(result.items); // [ 'a', 'b', 'b' ]The first call mutated prev.items, so the second call started from already-changed data. React discards one result, but the mutation already happened on the shared array, so the duplicate survives. The pure version is harmless to repeat:
const prev2 = { items: ['a'] };
const pure = p => ({ ...p, items: [...p.items, 'b'] });
pure(prev2);
const r = pure(prev2);
console.log(r.items, prev2.items); // [ 'a', 'b' ] [ 'a' ]There is a second reason purity matters. React compares the returned state with Object.is. If you mutate the previous array and return the same reference, React may decide nothing changed and skip the re-render entirely, which looks like “state did not update”.
Step-by-step walkthrough
Step 1: Decide whether you need the functional form
Use an updater when the next state depends on the previous state and more than one update might be queued before the next render: counters, toggles, appending to a list, or updates from intervals and subscriptions whose closures are stale.
setOpen(o => !o); // depends on previous
setItems(list => [...list, x]); // depends on previous
setName(event.target.value); // does not; plain value is clearerStep 2: Write the updater as a pure calculation
Read only the argument and values that are constant for this event (such as x above). Return new objects and arrays rather than editing existing ones:
setCart(cart => ({
...cart,
lines: cart.lines.map(line =>
line.id === id ? { ...line, qty: line.qty + 1 } : line
),
}));Every level that changes gets a fresh copy; levels that do not change keep their reference, which helps memoised children skip work.
Step 3: Move side effects out
Anything that talks to the outside world, such as fetch, localStorage, analytics, a toast, or another component’s setter, does not belong inside the updater. Compute what you need in the handler instead:
function handleAdd(product) {
setCart(cart => addLine(cart, product)); // pure
track('add_to_cart', { id: product.id }); // side effect, runs once
}If the side effect needs the resulting state, derive it in render or in an effect that reacts to the state change, rather than smuggling it into the updater.
Step 4: Keep Strict Mode on and treat duplicates as a signal
Strict Mode double-calling is not something to switch off. It is a cheap test that your updaters, reducers and component bodies are pure. If a value doubles only in development, look for mutation or side effects in one of those three places first. The Strict Mode double invoke guide lists everything React calls twice.
Worked scenario
A notifications panel keeps a list and an unread counter. The original code:
function onMessage(msg) {
setNotifications(list => {
list.unshift(msg); // mutation
setUnread(n => n + 1); // setter inside updater
navigator.vibrate?.(50); // side effect
return list;
});
}In development every message appears twice, the unread count jumps by two, and the device vibrates twice. In production the list sometimes fails to re-render, because list is returned by reference after mutation.
The fix splits calculation from effects:
function onMessage(msg) {
setNotifications(list => [msg, ...list]);
setUnread(n => n + 1);
navigator.vibrate?.(50);
}Now the updaters are pure, both setters are called once from the handler, and the vibration happens exactly once. Better still, if unread can be computed from the list (say, from a read flag), remove that state entirely and derive it during render.
Common mistake
- “React is buggy, it runs my code twice.” Only in development under Strict Mode, and only to reveal impurity. Production runs it once, but concurrent rendering can still re-run work, so impure code is fragile there too.
- Calling another setter inside an updater. It looks harmless but it is a side effect that will be duplicated, and it can trigger the “Cannot update a component while rendering” warning if the setter belongs to another component.
- Returning the mutated previous value.
list.push(x); return list;returns the same reference, so React can bail out of the render. - Thinking the functional form is “faster”. It is about correctness when updates queue up, not performance.
Verify the behavior
Wrap a component in <StrictMode> and log inside the updater:
import { StrictMode, useState } from 'react';
function Adder() {
const [items, setItems] = useState([]);
return (
<button onClick={() =>
setItems(prev => {
console.log('updater ran with length', prev.length);
return [...prev, prev.length];
})
}>
{items.join(',') || 'empty'}
</button>
);
}
export default function App() {
return <StrictMode><Adder /></StrictMode>;
}One click logs updater ran with length 0 twice (with React DevTools installed, the second log may appear dimmed), yet the button shows 0 once. Change the body to prev.push(prev.length); return [...prev]; and the button shows 0,0, the exact duplicate bug from the scenario.
Follow-up questions
Are reducers held to the same rule? Yes. A useReducer reducer is the same idea with named actions, and React double-calls it in Strict Mode for the same reason. See reducer invariants.
Can an updater read props? It can read values that are constant for the event, such as props captured by the handler. Those are fixed inputs, so purity holds.
Is Math.random() or Date.now() allowed in an updater? It breaks purity because two calls give different answers. Generate the id or timestamp in the handler and pass it in.
Does useState(initializer) follow the same rule? Yes. Initializer functions are also called twice in Strict Mode and must be pure.
Interview exercise
A teammate writes setTags(t => { t.add(newTag); return t; }) where tags is a Set. The UI does not update. Explain why and fix it.
Answer and reasoning
t.add mutates the existing Set and the updater returns the same reference. React compares old and new state with Object.is, sees the same object, and bails out of the re-render. Even when something else causes a render, the mutation is invisible to memoised children. The fix is to return a new set: setTags(t => new Set(t).add(newTag)). Set.prototype.add returns the set, so this is a pure, single-expression updater that is safe to call twice.
Continue learning
Keep practising with the React interview questions and the React MCQs. Related notes: state snapshots, Strict Mode double invoke and reducer invariants. Official reference: the useState updater function documentation and Queueing a Series of State Updates on react.dev.