ToolPopToolPop
React · Lesson 3 of 8

State, the box that remembers

11 min readUpdated 24 Jun 2026

Props come from above. State lives inside the component. The first time you call useState, you have officially crossed from "I render stuff" to "I build apps".

Diagram
rendering diagram...
The useState loop: setter triggers re-render, which reads the new value

useState in one paragraph

useState(initial) gives you back [value, setter]. The value is what to render. The setter, when called, tells React, "the value changed, please re-run this component". That re-run is a re-render. That is the whole thing.

jsx
function CartButton() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>
      Cart ({count})
    </button>
  );
}

The three rules

  • The setter triggers a re-render. Reassigning count = count + 1 does nothing. You must call setCount.
  • State is immutable. Never state.items.push(x). Make a new array: setItems([...items, x]). React compares by reference; mutating in place looks the same to it and the screen will not update.
  • Updates are batched. Calling setCount(count + 1) three times in one handler increments by one, not three. Use the function form when the new value depends on the old: setCount(c => c + 1).

The mutation rule trips up every Java or Python dev once. The first time I hit this bug, I was three hours into a Swiggy clone and my cart count just refused to budge.

Multiple useState vs one object

Most of the time, multiple useState calls win. Cleaner, easier to read, no spread-merge gymnastics.

jsx
// Good
const [name, setName] = useState('');
const [city, setCity] = useState('Bengaluru');
 
// Only when fields move together
const [form, setForm] = useState({ name: '', city: 'Bengaluru' });
setForm(f => ({ ...f, name: 'Riya' })); // spread the rest

If two pieces of state always change together (form fields, coordinates), grouping is fine. Otherwise, split.

The derived-state newbie bug

You have an array of orders. You want the total. The instinct is wrong:

jsx
const [orders, setOrders] = useState([]);
const [total, setTotal] = useState(0); // bug magnet

Now every place that updates orders must remember to update total. It will get out of sync by Friday.

Just calculate it. Every render. It is cheap.

jsx
const total = orders.reduce((sum, o) => sum + o.price, 0);

Rule of thumb: if you can compute it from existing state, do not store it. State is for things React cannot derive on its own.

Quick gotcha list

  • State updates are async. setCount(5); console.log(count); will log the old value.
  • Initial value runs every render but is only used once. For expensive setup, pass a function: useState(() => heavyCalc()).
  • Setting the same value skips the render. React bails out if Object.is(newValue, oldValue).

Next up: side effects, the part of React where most bugs are born.

Free tools you can use while you learn

Common questions

Q.Why is my state not updating immediately?
A.Because React batches state updates and re-renders. The state value you see right after calling setState is still the old one. If you need the new value in a callback, use the functional setState form: setState(prev => prev + 1).
Q.When should I use useState vs useReducer?
A.useState for one or two simple values. useReducer when state has multiple related fields, or transitions depend on the previous state, or you find yourself writing the same setState patterns over and over.
Chai0/1 done

Watching quietly. Tap me if you want a tip.

React Playground
preview

Try this (0 of 1 done)

  1. 1

    Add a reset button that sets count back to 0.

    show answer
    function App() {
      const [count, setCount] = React.useState(0);
      return (
        <div>
          <button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
          {' '}
          <button onClick={() => setCount(0)}>reset</button>
        </div>
      );
    }