By Ryan Calloway. Updated May 2026.
A 2022 r/reactjs thread opens with “I am using a simple useEffect hook to fetch data from an API and then using useState… when I console log the variable after I have used useState, it shows as undefined”. The top reply is the answer almost every time: state is asynchronous, the snapshot you logged is the old render, the new value arrives next tick. The other reasons that produce “useState not updating” land in the r/react “changing object in useState” thread (mutating the same object reference) and a 2024 React GitHub issue on Promise updaters running twice in StrictMode. Six reasons cover the rest.
Quick answer
React state updates are asynchronous and work through replacement, not mutation. If your value looks wrong after setX(newValue), you are either (1) reading the old closure in the same tick, (2) mutating the existing object instead of creating a new one, (3) logging on the same tick before the re-render, (4) racing a parent re-render that resets the value, (5) racing an async fetch whose stale promise resolves last, or (6) confused by Strict Mode’s intentional double-fire in development. The 80%-of-the-time fix is the functional updater: setX(prev => ...). It uses the latest queued value, not whatever x was when the closure was created.
How to diagnose in 30 seconds
- Add a render-side log (runs every render):
console.log("RENDER", count); - Add an effect-side log (runs only when the value actually changes):
useEffect(() => { console.log("CHANGED", count); }, [count]); - Click the thing. Read both logs.
RENDERshows the new value,CHANGEDfires once → React is fine; your bug is readingcounton the same tick aftersetCount. Cause 3.RENDERnever shows the new value, the setter ran → the new state equals the old state by reference (mutation) or by value (no-op). Cause 2.RENDERshows the new value, then immediately reverts → something else is overwriting. Cause 4.- Multiple
setXin the same handler, only the last one wins → Cause 1 (stale closure). - The state changes after a stale fetch resolves and clobbers the new one → Cause 5.
- Effects or initializers run twice in dev only → Cause 6 (Strict Mode is doing what it says).
Cause 1: stale closure (functional updater is the universal fix)
Each render captures count at the moment it ran. Calling setCount(count + 1) twice in the same handler schedules the same new value twice. React 18 and 19 batch all state updates, including those inside Promises and timers, so multiple plain-value setters in one tick collapse to one update.
function Counter() {
const [count, setCount] = useState(0);
function addTwo() {
setCount(count + 1); // queues count: 0 -> 1
setCount(count + 1); // queues count: 0 -> 1 again
} // final: 1, not 2
}
Fix — use the functional updater. The argument is always the latest queued state, even when batched.
function addTwo() {
setCount(c => c + 1); // queues 0 -> 1
setCount(c => c + 1); // queues 1 -> 2
} // final: 2
The React useState reference calls this out: “always use an updater function when the new state depends on the old”. Same fix when state lives behind setTimeout, fetch().then(...), or any callback that fires after the closure was captured.
Cause 2: mutating state instead of replacing it
React compares by reference, not by content. Mutate the same array or object and pass it back, the setter sees the same reference and skips the re-render.
// Wrong
const [items, setItems] = useState([1, 2, 3]);
function addItem(x) {
items.push(x); // mutates in place
setItems(items); // same reference, no re-render
}
// Right
function addItem(x) {
setItems(prev => [...prev, x]);
}
// Wrong — same reference returned from the updater
setUser(u => { u.name = "Ryan"; return u; });
// Right
setUser(u => ({ ...u, name: "Ryan" }));
Mental rule: if you cannot replace the entire object with the spread operator, you are mutating. Immer is the escape hatch for deeply nested shapes; it lets you write mutating-style code and produces new references under the hood. With the React Compiler (beta since late 2024, on a path to stable in React 19.x) auto-memoizing components, this rule matters even more — the compiler relies on referential change to know when to re-run.
Cause 3: logging the wrong tick
The state did update. Your console.log is from the previous render. This is the literal subject of the r/reactjs thread linked at the top.
setUsers([...users, newUser]);
console.log(users); // still the old array; this tick has not re-rendered
Fix — log inside the component body (runs every render) or inside an effect that watches the value:
useEffect(() => {
console.log("users changed:", users);
}, [users]);
This catches the majority of “it is not updating” tickets in any React codebase. The state was updating; the log was lying.
Cause 4: a parent or effect overwrites your update
You call setX(newValue), it works for one tick, then something else clobbers it. Usual suspects:
- A parent component re-rendering with a prop that initializes the child’s state on every render.
- A
useEffectwith missing or incorrect dependencies that keeps resetting the value. - A form library (Formik, react-hook-form) that owns the state; you are updating the wrong one.
- A global store (Zustand, Redux) and a local
useStatefor the same value, drifting out of sync.
What it looks like:
// Bug: resets on every parent render
useEffect(() => {
setFilter(defaultFilter);
}, [defaultFilter]); // defaultFilter is a fresh object each parent render
Fix — either initialize once via useState and stop resetting, or stabilize the prop with useMemo in the parent. The “You might not need an effect” guide is the right reading for this category.
const [filter, setFilter] = useState(defaultFilter); // runs once on mount
Diagnostic: add console.log("setFilter called with", newValue, new Error().stack) inside every setter path. Two writers means two stacks; find the second one and delete it.
Cause 5: async race — the stale fetch wins
You set state from a fetch. The user navigates, kicking off a second fetch. The first fetch was slower; it resolves last and overwrites the correct value with stale data.
useEffect(() => {
fetch(`/api/users/${id}`)
.then(r => r.json())
.then(setUser);
}, [id]);
// id changes 1 -> 2 -> 3 quickly; the response for id=1 may resolve last and overwrite id=3.
Fix — abort the previous fetch in the cleanup, or guard with the request id:
useEffect(() => {
const ctrl = new AbortController();
fetch(`/api/users/${id}`, { signal: ctrl.signal })
.then(r => r.json())
.then(setUser)
.catch(e => { if (e.name !== "AbortError") throw e; });
return () => ctrl.abort();
}, [id]);
For data fetching at scale, this is what TanStack Query, SWR, and React 19’s use() hook with Suspense exist to handle. The use() reference covers the new Suspense-friendly read; React Server Components handle the same race entirely server-side. If you find yourself writing custom abort logic in three places, lift fetching out to a library or Server Component and let it handle the race.
Cause 6: Strict Mode double-fire (this is not a bug)
React 18 and 19 deliberately invoke state initializers, render functions, and effects twice in development under <StrictMode> to surface impure side effects. If you see setX firing twice in dev only, that is the design, not your code. It does not happen in production.
// Strict Mode: this initializer runs twice in dev, once in prod
const [token, setToken] = useState(() => {
console.log("init"); // dev: logs twice; prod: once
return crypto.randomUUID();
});
The Strict Mode reference spells out which functions are double-invoked. The fix is never to disable Strict Mode; it is to make the initializer or effect idempotent. If a side effect cannot tolerate double invocation (a POST to a non-idempotent endpoint), move it out of an effect entirely — into an event handler, a Server Action (React 19), or a mutation hook from your data library. The 2024 issue facebook/react#29829 is exactly this: developers treating Strict Mode’s correctness probe as a regression.
Decision tree: which cause is yours
- Multiple
setXin the same handler, only the last took effect → Cause 1. Switch tosetX(prev => ...). - Setter ran but the component never re-rendered → Cause 2. You returned the same reference. Spread or use Immer.
- The component renders the new value but your
console.logshows the old → Cause 3. Move the log into an effect. - The new value flashes for one render and then reverts → Cause 4. Find the second writer (parent prop, effect, library).
- The wrong value comes from an async fetch you fired earlier → Cause 5. Abort or use a data library.
- Effects or initializers run twice in dev, once in prod → Cause 6. Make them idempotent. Strict Mode is correct.
The functional updater is the single most useful pattern in this list. setX(prev => next(prev)) fixes Causes 1 and 2 outright and prevents most flavors of 4 and 5. Reach for it by default; reach for plain values only when the new value is independent of the previous one.
FAQ
Why does my state have the old value after I call setState?
React state is asynchronous. The variable you declared with useState is a snapshot of the render where it was declared; it does not change on the same tick. Read the new value in the next render (via useEffect) or compute the new value locally before the setter call.
Should I use setState(prev => prev + 1) or setState(state + 1)?
Use the functional form (prev =>) whenever the new state depends on the old. Use the value form only when the new state is independent, like setFilter("active"). The functional form is also the only correct choice when the setter runs inside a Promise, timer, or event handler that may fire after a re-render.
Does mutating an array inside state ever work?
No. React sees the same reference and skips the re-render. Use map for changes, the spread for appending, filter for removing, or Immer for deeply nested shapes.
Is React 18’s automatic batching different in React 19?
The behavior is the same: every setX in a single tick — in event handlers, Promises, setTimeout, native event handlers — is batched into one render. React 19 added the Compiler (beta), which auto-memoizes components and hooks, removing most cases where you would write useMemo/useCallback by hand.
Why does useEffect run twice in development?
Strict Mode invokes effects twice in dev to surface missing cleanups and non-idempotent side effects. The second run is a feature, not a bug. Make the effect idempotent (return a cleanup, or move non-idempotent work out of effects) and the doubling becomes invisible.
Is useState going away in React 19?
No. React 19 added useActionState, useOptimistic, and the use() hook, plus the Compiler, all on top of existing hooks. useState is core. The React 19 release post has the full list.
Sources and further reading
- React —
useStatereference - React —
StrictMode - React — Queueing a series of state updates
- React 19 release notes
- React — You might not need an effect
Next steps
If the bug moved from “not updating” to “updating twice” once you shipped a fix, the next thing to read is the useEffect cleanup guide, which covers the subscription and timer patterns that produce double updates. For the TypeError that shows up when a state update races a render, the Cannot read properties of undefined fix is the companion piece.