Problem Solved

React · state not updating

Home › Courses › React › Problems › State not updating
⚛️ Problem · 5 min read

"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.

async-state.tsx
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)} />

Saved: {saved}

); } // ─── 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);
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.

functional-update.tsx
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
({ name: '', email: '' }); function clearAll() { // ✅ Functional — always uses the latest state setForm(f => ({ ...f, name: '', email: '' })); } function updateName(name: string) { setForm(f => ({ ...f, name })); } return (
updateName(e.target.value)} />
); } // ─── When to use functional setState ───────── // // ✅ Whenever the new value depends on the old value: // setCount(c => c + 1) // setItems(prev => [...prev, item]) // setForm(f => ({ ...f, name })) // // ✅ Whenever you set state inside a callback or effect: // The closure might be stale — functional form always works. // // ❌ When the new value is independent: // setName('Alice') — fine, no need for functional // setLoading(true) — fine console.log(BadDouble, GoodDouble, BadConfirm, GoodConfirm, FormExample);
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.

immutable-updates.tsx
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 (

{user.name}

); } // ─── GOOD: create new object ───────────────── function GoodUpdate() { const [user, setUser] = useState({ name: 'Alice', age: 30 }); function updateName() { setUser({ ...user, name: 'Bob' }); // ✅ new object } return (

{user.name}

); } // ─── 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 (
    {items.map((item, i) => (
  • {item}
  • ))}
); } // ─── Nested objects: spread every level ────── interface Profile { name: string; address: { city: string; zip: string }; } function NestedUpdate() { const [profile, setProfile] = useState({ 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.

1. Logging state right after setState

Sees the old value. Fix: log in useEffect, or compute the new value locally first.

2. Calling setCount(count + 1) twice

Only +1 total. Fix: setCount(c => c + 1) twice.

3. Mutating state objects

user.name = 'x'; setUser(user) — no re-render. Fix: setUser({ ...user, name: 'x' }).

4. Using array.push on state

Mutates, no re-render. Fix: setItems([...items, newItem]).

5. Expecting setState in an effect to be synchronous

The effect runs after render — its setState triggers another render. That's expected.

6. Storing derived state

setFull(first + ' ' + last) in render — infinite loop. Fix: compute during render, no state.

7. Using await after setState

setX(1); await fetch(); console.log(x) — x is still old after await unless another render happened. Fix: capture the value.

8. Passing stale state to a callback

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' }).

🔗 Related

← All React problems
Problem solved · React