Quick Answer

useState returns the current value and a setter. Calling the setter schedules a re-render rather than changing the variable immediately, so reading the state right after setting it gives the old value. Use the functional form when the new value depends on the previous one, and always create a new object or array rather than mutating, because React compares by reference to decide whether to re-render.

The Basics, and the First Surprise

const [count, setCount] = useState(0);

Array destructuring gives you the current value and a function to change it. The argument is the initial value, used only on the first render.

The first thing that surprises people:

function handleClick() {
  setCount(count + 1);
  console.log(count);      // logs the OLD value, not the new one
}

count is a constant within this render. Calling the setter does not reassign it — it tells React to render the component again, and on that next render count will be a new constant with the new value.

So state is not a variable you mutate. It is a value React hands you per render, plus a way to request the next one.

The consequence people hit next:

setCount(count + 1);
setCount(count + 1);      // total increase: 1, not 2

Both calls read the same count from this render. Both compute the same result. React batches them and the second simply overwrites the first.

The Functional Updater

When the new value depends on the previous one, pass a function instead of a value. React calls it with the latest state, including updates queued in the same batch.

setCount(c => c + 1);
setCount(c => c + 1);      // now increases by 2

Use it whenever you are deriving from the current state — incrementing, toggling, appending.

setIsOpen(open => !open);              // toggle
setItems(prev => [...prev, newItem]);  // append

It also solves the stale closure problem, which is subtler. A callback created in one render captures that render's values forever:

useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1);      // count is always 0 here — captured on first render
  }, 1000);
  return () => clearInterval(id);
}, []);                       // empty deps: the effect never sees a newer count

The counter goes 0, 1, 1, 1… The functional form fixes it without touching the dependency array, because it does not read count at all:

setCount(c => c + 1);         // always operates on the latest value

This pattern — intervals, subscriptions, event listeners registered once — is where the functional updater stops being a style preference and becomes the only correct option.

Updating Objects and Arrays

React decides whether to re-render by comparing the new state to the old by reference. Mutating an object keeps the same reference, so React sees no change.

// Nothing happens — same array, same reference
items.push(newItem);
setItems(items);

// Correct — a new array
setItems([...items, newItem]);

The patterns worth memorising:

// Add
setItems([...items, newItem]);

// Remove
setItems(items.filter(i => i.id !== id));

// Update one item
setItems(items.map(i => i.id === id ? { ...i, done: true } : i));

// Update one field of an object
setUser({ ...user, name: 'Riya' });

// Update a nested field — spread at every level you change
setUser({
  ...user,
  address: { ...user.address, city: 'Pune' }
});

That last one is the trap. Spread is shallow, so { ...user } shares the same address object. Mutating it changes both copies and React still sees no change at the top level.

Also note which array methods mutate: push, pop, splice, sort and reverse all modify in place. Copy first — [...items].sort() — or use the non-mutating toSorted where available.

If nested updates become painful, that is usually a signal to flatten the state shape rather than to write deeper spreads.

Structuring State Well

Do not store what you can compute. This is the most valuable habit here.

// Two sources of truth that can drift apart
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);

// One source of truth — total cannot be wrong
const [items, setItems] = useState([]);
const total = items.reduce((sum, i) => sum + i.price, 0);

Every derived value kept in state is a value that can become inconsistent. Filtered lists, counts, totals and "is the form valid" should almost always be computed during render.

Group state that changes together, split state that does not. A form with five fields updated independently is fine as five useState calls, or as one object — but if you use one object, remember to spread the rest:

setForm({ ...form, email: value });    // forgetting the spread loses the other fields

Use lazy initial state for expensive setup. The argument to useState is evaluated on every render, even though only the first value is used:

useState(expensiveCompute());       // runs every render, result discarded
useState(() => expensiveCompute()); // runs once

This matters when reading from localStorage or parsing something large.

State is per component instance. Two instances of the same component have completely independent state — which is obvious once stated but a common early confusion.

Problems and Their Causes

"My component does not re-render." You mutated instead of replacing. Check for push, direct property assignment, or a nested object shared through a shallow spread.

"State resets unexpectedly." The component is unmounting and remounting. Usually the parent is rendering it under a different key, or the component is defined inside another component so it is a new type on every render:

function Parent() {
  function Child() { ... }    // new function identity each render — state lost
  return <Child />;
}

Define components at module level.

"Too many re-renders." You called the setter during render rather than in a handler or effect:

function Bad() {
  const [n, setN] = useState(0);
  setN(n + 1);              // render → update → render → ...
  return <p>{n}</p>;
}

<button onClick={setCount(1)}>      // calls immediately during render
<button onClick={() => setCount(1)}>  // correct — passes a function

"An input will not type." A controlled input with value but no onChange is locked to the state value. Add the handler, or use defaultValue for an uncontrolled input.

"Props changed but state did not." The initial value is only used on the first render. Deriving state from props is usually a design smell — compute during render, or reset with a key prop when the identity genuinely changes.

Frequently Asked Questions

Why does my state show the old value after setting it? Because the state variable is a constant within the current render. Calling the setter schedules a re-render rather than reassigning it, so the new value only exists in the next render. Use the functional updater if you need to build on the previous value.
When should I use the functional form of the setter? Whenever the new value depends on the old one — incrementing, toggling, appending — and always inside intervals, subscriptions or event listeners registered once, where a captured value would otherwise go stale.
Why does pushing to an array not re-render? React compares by reference, and push mutates the existing array so the reference is unchanged. Create a new array with spread or filter or map instead. The same applies to objects and to sort and reverse.
Should I use one state object or several useState calls? Several is usually simpler for unrelated values, since each updates independently. Group into one object when the values genuinely change together — and remember to spread the existing object so you do not drop the other fields.
Why does my component re-render infinitely? You are calling the setter during render rather than in an event handler or effect. A common variant is writing onClick={setCount(1)}, which calls it immediately, instead of onClick={() => setCount(1)}.