React useState Explained: How State Actually Works
useState gives a component memory that survives re-renders and updates the screen when it changes. How it works, why the value doesn't change immediately after you set it, updating objects and arrays without mutating them, and the functional update form.
useState is the first React hook everyone meets and the source of most beginner confusion. Once you understand three rules, almost every "why isn't my state updating?" bug becomes obvious.
What state is
A React component is a function that runs every time the screen needs updating. Normal variables inside it are reset on every run:
function Counter() {
let count = 0 // reset to 0 on every render
return <button onClick={() => count++}>{count}</button> // never changes on screen
}
State is a variable React remembers between runs. And when you change it, React re-runs the component so the screen shows the new value.
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
useState(0) returns two things: the current value (count) and a function to change it (setCount). 0 is the starting value, used only on the first render.
Rule 1: Setting state doesn't change the variable right now
function handleClick() {
setCount(count + 1)
console.log(count) // still the old value!
}
setCount doesn't change count in the running code. It asks React to re-render with the new value. The current function call keeps seeing the old one. The new value shows up on the next render.
So if you need the new value immediately, compute it into a local variable:
const next = count + 1
setCount(next)
saveToServer(next)
Rule 2: Use the function form when the new value depends on the old
setCount(count + 1)
setCount(count + 1) // count is still the old value → only +1 total
Pass a function instead, and React gives you the latest value:
setCount(c => c + 1)
setCount(c => c + 1) // +2 total ✅
Use this form whenever the update depends on the previous state — counters, toggles (setOpen(o => !o)), appending to lists — especially inside timers and async code where the variable may be stale.
Rule 3: Never mutate objects and arrays — replace them
React decides whether to re-render by checking if the value is a different object. If you change an object in place, it's the same object, so nothing happens:
user.name = 'Sam'
setUser(user) // ❌ same object, no re-render
Create a new one with the spread operator:
setUser({ ...user, name: 'Sam' }) // ✅
Arrays work the same way. Use methods that return a new array:
| Want to | Do |
|---|---|
| Add | setItems([...items, newItem]) |
| Remove | setItems(items.filter(i => i.id !== id)) |
| Update one | setItems(items.map(i => i.id === id ? { ...i, done: true } : i)) |
Avoid push, splice, sort and direct assignment on state arrays — they mutate. (toSorted() returns a new array.) (JavaScript array methods)
How much state do you need?
Less than you think. Don't store anything you can calculate:
const [items, setItems] = useState([])
const total = items.reduce((sum, i) => sum + i.price, 0) // not state
Storing derived values in state means keeping two things in sync, which is where bugs come from.
When state lives in the wrong place
If two components need the same state, move it up to their closest shared parent and pass it down as props ("lifting state up"). When that gets awkward across many levels, look at context or a small state library. (React state management)
The summary
- State is memory React keeps between renders; changing it triggers a re-render.
- The variable doesn't change until the next render.
- Use
setX(prev => …)when the new value depends on the old. - Replace objects and arrays; never mutate them.
- Don't store what you can compute.
EasySpawn gives Claude Code a persistent server with your app running, so it can trace a state bug in the live preview, fix it, and keep going while you're away. See how it works or join the waitlist.
Related: useEffect Explained · Too Many Re-renders · What Is React? · The Unique Key Prop Warning
Keep reading
What Is TypeScript? JavaScript With Labels, Explained for Beginners
TypeScript is JavaScript plus types: labels that say what kind of value each thing holds, checked before your code runs. What it looks like, why AI tools default to it, what .ts and .tsx files are, and how to read the red squiggles.
What Is a Headless CMS? And Does Your App Need One?
A headless CMS is a content editor with no website attached — it stores your content and hands it to your app through an API. How it differs from WordPress, the popular options, when it beats Markdown files or your own database, and when it's overkill.