"State is not updating."
Three causes, one mental model.
You call setCount(count + 1) and log count — it's still the old value. Or you call it twice and the count only goes up by one. Or you update an object and nothing renders. React state is asynchronous, batched, and compared by reference. Once you internalize these three facts, every "state isn't updating" bug becomes obvious.
Short answer: State updates are asynchronous. Calling setCount(5) doesn't change count in the current render — it schedules a new render with 5. If you log count right after, you see the old value. For sequential updates: setCount(c => c + 1) (functional form). For objects: setUser({ ...user, name: 'x' }) (new reference). Never mutate state in place.
✅ Solution 1 — State is async (log after render, not after setState)
The most common confusion — the state just hasn't changed yet.
When you call setState, React doesn't immediately update the state. It schedules a re-render. The state variable inside the current render keeps its old value — that's just how closures work. If you need the new value, compute it locally (const newCount = count + 1) and use that. Or move the log into an effect that runs after the render.
import { useState, useEffect } from 'react'; // ─── BAD: expecting immediate update ───────── function BadCounter() { const [count, setCount] = useState(0); function increment() { setCount(count + 1); console.log(count); // ❌ logs 0, not 1 // setCount schedules an update, doesn't change count } return ; } // ─── GOOD 1: compute the new value locally ─── function GoodCounter1() { const [count, setCount] = useState(0); function increment() { const next = count + 1; setCount(next); console.log(next); // ✅ logs 1 } return ; } // ─── GOOD 2: log in an effect ──────────────── function GoodCounter2() { const [count, setCount] = useState(0); useEffect(() => { console.log('count is now:', count); // ✅ runs after render }, [count]); return ( ); } // ─── GOOD 3: use the value at the point you need it function FormExample() { const [name, setName] = useState(''); const [saved, setSaved] = useState(''); function save() { // Use name directly — it's the current value from this render setSaved(name); fetch('/api/save', { method: 'POST', body: JSON.stringify({ name }) }); } return (setName(e.target.value)} />); } // ─── The mental model ───────────────────────── // // Every render creates a snapshot: // Render 1: count = 0. Functions capture count = 0. // setCount(1) schedules render 2. // Render 2: count = 1. New functions capture count = 1. // // Inside one render, count is CONSTANT. // setCount doesn't change it — it schedules the next render. console.log(BadCounter, GoodCounter1, GoodCounter2, FormExample);Saved: {saved}
import { useState, useEffect } from 'react'; function BadCounter() { const [count, setCount] = useState(0); function increment() { setCount(count + 1); console.log(count); } return React.createElement('button', { onClick: increment }, count); } function GoodCounter1() { const [count, setCount] = useState(0); function increment() { const next = count + 1; setCount(next); console.log(next); } return React.createElement('button', { onClick: increment }, count); } function GoodCounter2() { const [count, setCount] = useState(0); useEffect(() => { console.log('count is now:', count); }, [count]); return React.createElement('button', { onClick: () => setCount(c => c + 1) }, count); } function FormExample() { const [name, setName] = useState(''); const [saved, setSaved] = useState(''); function save() { setSaved(name); fetch('/api/save', { method: 'POST', body: JSON.stringify({ name }) }); } return React.createElement('div', null, React.createElement('input', { value: name, onChange: e => setName(e.target.value) }), React.createElement('button', { onClick: save }, 'Save'), React.createElement('p', null, 'Saved: ', saved) ); } console.log(BadCounter, GoodCounter1, GoodCounter2, FormExample);
Every render is a snapshot
The count variable is captured at render time. Functions inside that render see the same value forever.
setState schedules
setCount(5) doesn't set count to 5 immediately. It schedules a render where count will be 5.
Compute new values locally
If you need to use the new value in the same handler, compute it: const next = count + 1. Then use next.
Log in an effect
To see the new value, log it in a useEffect with the state as dependency. Runs after the render, sees the new value.
✅ Solution 2 — Sequential updates need the functional form
The bug that trips up every beginner — and some intermediate devs.
Calling setCount(count + 1) twice only increments by one. Both calls capture the same count, compute the same result, and React batches them — the last one wins. The fix is the functional form: setCount(c => c + 1). Each call receives the latest pending value, so increments stack correctly.
import { useState } from 'react'; // ─── BAD: two increments, only +1 total ────── function BadDouble() { const [count, setCount] = useState(0); function incrementTwice() { setCount(count + 1); // 0 + 1 = 1 setCount(count + 1); // 0 + 1 = 1 (same count!) // Both compute 1. React batches → applies 1. // Result: count goes from 0 to 1, not 0 to 2. } return ; } // ─── GOOD: functional updates stack ────────── function GoodDouble() { const [count, setCount] = useState(0); function incrementTwice() { setCount(c => c + 1); // 0 → 1 (c receives 0) setCount(c => c + 1); // 1 → 2 (c receives 1) // Each call receives the previous pending value → stacks } return ; } // ─── BAD: reading state after setState ─────── function BadConfirm() { const [value, setValue] = useState(''); function submit() { setValue('submitted'); // if (value === 'submitted') — never true on this click alert(value); // ❌ still old value } return ; } // ─── GOOD: use the value directly ──────────── function GoodConfirm() { const [value, setValue] = useState(''); function submit() { const newValue = 'submitted'; setValue(newValue); alert(newValue); // ✅ uses the computed value } return ; } // ─── Object state with functional update ───── interface Form { name: string; email: string } function FormExample() { const [form, setForm] = useState
import { useState } from 'react'; function BadDouble() { const [count, setCount] = useState(0); function incrementTwice() { setCount(count + 1); setCount(count + 1); } return React.createElement('button', { onClick: incrementTwice }, count); } function GoodDouble() { const [count, setCount] = useState(0); function incrementTwice() { setCount(c => c + 1); setCount(c => c + 1); } return React.createElement('button', { onClick: incrementTwice }, count); } function BadConfirm() { const [value, setValue] = useState(''); function submit() { setValue('submitted'); alert(value); } return React.createElement('button', { onClick: submit }, 'Submit'); } function GoodConfirm() { const [value, setValue] = useState(''); function submit() { const newValue = 'submitted'; setValue(newValue); alert(newValue); } return React.createElement('button', { onClick: submit }, 'Submit'); } function FormExample() { const [form, setForm] = useState({ name: '', email: '' }); function clearAll() { setForm(f => ({ ...f, name: '', email: '' })); } function updateName(name) { setForm(f => ({ ...f, name })); } return React.createElement('div', null, React.createElement('input', { value: form.name, onChange: e => updateName(e.target.value) }), React.createElement('button', { onClick: clearAll }, 'Clear') ); } console.log(BadDouble, GoodDouble, BadConfirm, GoodConfirm, FormExample);
Same closure, same value
Two setCount(count + 1) calls use the same count. Both compute the same result.
Functional updates stack
setCount(c => c + 1) — each call receives the latest pending value. Order is preserved.
Batching is automatic
React batches all setState calls in the same event handler. One re-render, all updates applied in order.
Default to functional
When the new value depends on the old: always functional. It's one character longer and always correct.
Adopt the functional form as a habit — never type setCount(count + 1) again. Even when there's only one update, the functional form is safer: it always sees the latest value, even if a previous effect, callback, or timer is also updating the state. The two-character cost (c => c + 1 vs count + 1) buys you immunity from a whole class of bugs. Every production React codebase I've seen that "sometimes loses an increment" was using the direct form somewhere.
✅ Solution 3 — Objects and arrays need new references
Mutating state in place never re-renders.
React compares state by reference. If you mutate an object (user.name = 'x') and then setUser(user), React sees the same reference — no change, no re-render. Fix: create a new object with the spread operator. Same for arrays. The mutation might work in some cases but it's fragile — always create new references.
import { useState } from 'react'; interface User { name: string; age: number } // ─── BAD: mutation, no re-render ───────────── function BadMutate() { const [user, setUser] = useState({ name: 'Alice', age: 30 }); function updateName() { user.name = 'Bob'; // mutate setUser(user); // same reference → no re-render } return ( ); } // ─── GOOD: create new object ───────────────── function GoodUpdate() { const [user, setUser] = useState{user.name}
({ name: 'Alice', age: 30 }); function updateName() { setUser({ ...user, name: 'Bob' }); // ✅ new object } return ( ); } // ─── Arrays: new reference, not mutation ───── function ArrayUpdates() { const [items, setItems] = useState<string[]>(['a', 'b']); // ✅ Add function add(item: string) { setItems(prev => [...prev, item]); } // ✅ Remove function remove(item: string) { setItems(prev => prev.filter(i => i !== item)); } // ✅ Update at index function update(index: number, value: string) { setItems(prev => prev.map((item, i) => i === index ? value : item)); } // ❌ Never do these: // items.push('c'); // mutates, no re-render // items[index] = 'x'; // mutates, no re-render // items.splice(0, 1); // mutates, no re-render return ({user.name}
); } // ─── Nested objects: spread every level ────── interface Profile { name: string; address: { city: string; zip: string }; } function NestedUpdate() { const [profile, setProfile] = useState{items.map((item, i) => (
- {item}
))}({ name: 'Alice', address: { city: 'Springfield', zip: '12345' } }); function updateCity(city: string) { // ✅ Spread at both levels setProfile(p => ({ ...p, address: { ...p.address, city } })); } return {profile.address.city}
; } // ─── The rule ──────────────────────────────── // // State is IMMUTABLE. Every update creates a new reference. // Object: { ...prev, changed: value } // Array: [...prev, newItem], prev.filter(...), prev.map(...) // Nested: { ...prev, nested: { ...prev.nested, changed: value } } // // Mutating prev.changed = value never triggers a re-render. // React compares old reference === new reference. Same → no update. console.log(BadMutate, GoodUpdate, ArrayUpdates, NestedUpdate);
import { useState } from 'react'; function BadMutate() { const [user, setUser] = useState({ name: 'Alice', age: 30 }); function updateName() { user.name = 'Bob'; setUser(user); } return React.createElement('div', null, React.createElement('p', null, user.name), React.createElement('button', { onClick: updateName }, 'Change to Bob') ); } function GoodUpdate() { const [user, setUser] = useState({ name: 'Alice', age: 30 }); function updateName() { setUser({ ...user, name: 'Bob' }); } return React.createElement('div', null, React.createElement('p', null, user.name), React.createElement('button', { onClick: updateName }, 'Change to Bob') ); } function ArrayUpdates() { const [items, setItems] = useState(['a', 'b']); function add(item) { setItems(prev => [...prev, item]); } function remove(item) { setItems(prev => prev.filter(i => i !== item)); } function update(index, value) { setItems(prev => prev.map((item, i) => i === index ? value : item)); } return React.createElement('div', null, React.createElement('button', { onClick: () => add('new') }, 'Add'), React.createElement('ul', null, items.map((item, i) => React.createElement('li', { key: item }, item, React.createElement('button', { onClick: () => remove(item) }, '×') ) ) ) ); } function NestedUpdate() { const [profile, setProfile] = useState({ name: 'Alice', address: { city: 'Springfield', zip: '12345' } }); function updateCity(city) { setProfile(p => ({ ...p, address: { ...p.address, city } })); } return React.createElement('p', null, profile.address.city); } console.log(BadMutate, GoodUpdate, ArrayUpdates, NestedUpdate);
Reference equality
React compares oldState === newState. Mutating oldState keeps the same reference — no re-render.
Spread for objects
{ ...prev, changed: value } — copies all fields, overrides one. New reference, re-render.
Array methods return new
filter, map, slice, concat — return new arrays. push, splice, sort — mutate, never use on state.
Nested needs deep spread
Every level of nesting needs its own spread. { ...prev, address: { ...prev.address, city } }.
Mutation sometimes appears to work — but it's broken. React might re-render anyway because a parent re-rendered, masking the bug. Then you ship, a user reports "the delete button doesn't update the list," and you can't reproduce it because it depends on timing. The rule is absolute: never mutate state. Create new objects and arrays. Use Immer if the nested spreads get too noisy — it lets you write mutable-looking code that produces immutable updates.
⚠️ Common mistakes
The state update bugs that cost real time.
Sees the old value. Fix: log in useEffect, or compute the new value locally first.
setCount(count + 1) twiceOnly +1 total. Fix: setCount(c => c + 1) twice.
user.name = 'x'; setUser(user) — no re-render. Fix: setUser({ ...user, name: 'x' }).
array.push on stateMutates, no re-render. Fix: setItems([...items, newItem]).
The effect runs after render — its setState triggers another render. That's expected.
setFull(first + ' ' + last) in render — infinite loop. Fix: compute during render, no state.
await after setStatesetX(1); await fetch(); console.log(x) — x is still old after await unless another render happened. Fix: capture the value.
The callback captures old count. Fix: functional setter, or pass the value at call time.
❓ Frequently asked
Why is my React state not updating immediately after setState?
React state updates are asynchronous. Calling setState doesn't change the current render's value — it schedules a re-render with the new value. Inside the same event handler or effect, the state variable still holds the old value. If you need the new value immediately, compute it locally or use a functional update.
Why does calling setState twice only increase the count by one?
Because setState doesn't return the new value — it schedules an update. Calling setCount(count + 1) twice uses the same captured count value both times, so both calls compute the same result. Fix: use the functional form: setCount(c => c + 1) twice.
When does React actually re-render after setState?
React 18+ batches state updates and re-renders at the end of the event handler (or the next microtask). Inside the same synchronous block, state doesn't change. In an async function with await, each await boundary flushes pending state.
Why doesn't my state update when I pass an object?
React compares state by reference, not by value. If you mutate the object (obj.name = 'x') and then setState(obj), React sees the same reference and skips the re-render. Fix: create a new object: setState({ ...obj, name: 'x' }).