Skip to content

State

A regular variable inside a function is forgotten the moment the function finishes, so it can't hold a value that changes over time and shows up on screen. That gap is what state fills: it is how a component remembers something between renders. Where props are data passed in from the outside that a component never changes, state is data the component owns and updates itself. The useState hook gives a component that memory, and it gives you a way to update the value that also tells React to redraw.

Here's a counter:

jsx
import { useState } from 'react'

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

useState(0) sets the starting value and hands back a pair: the current value, count, and a function to change it, setCount. The names are yours; the [value, setValue] shape is the convention. The button's onClick runs a function each time it is clicked, which the Events chapter covers in full next, so take it on trust for now. That function calls setCount(count + 1), and the call does two things. It stores the new value, and it re-renders Counter so the screen shows the updated number. Calling the setter is the signal to React that something changed and the component should run again.

Updating based on the previous value

You'll often see the setter called with a function instead of a plain value:

jsx
setCount(c => c + 1)

This is the updater form. React calls your function with the latest value and uses whatever you return as the next state. Reach for it whenever the new value is built from the old one, and especially when you update more than once in a row. These two lines do not do what they look like:

jsx
setCount(count + 1)
setCount(count + 1)

Both reads of count see the same value from this render, so together they only move the count up by one. The updater form fixes it, because each call receives the result of the one before it:

jsx
setCount(c => c + 1)
setCount(c => c + 1)

Now the count goes up by two. Flipping a boolean works the same way and is where you'll meet the updater form most often: setIsOpen(open => !open) takes the current value and returns its opposite. When your next value depends on the current one, the updater form is the safe default.

State is per component instance

Each time you render a component, it gets its own separate state. Two counters on the same page each keep their own count:

jsx
function App() {
  return (
    <>
      <Counter />
      <Counter />
    </>
  )
}

Clicking the first button does nothing to the second. They share the same Counter code, and each one holds an independent value. The state belongs to each instance on the screen, so one function can drive many separate copies.

Replace state, don't mutate it

You update state by giving React a new value, and React compares that value to the old one to decide what changed. Changing the existing value in place skips that signal, so the screen goes stale. This matters most with objects and arrays. To add to an array, build a new array rather than pushing into the old one:

jsx
// Skips the update: React never sees a new value
todos.push(newTodo)
setTodos(todos)

// Works: a brand new array
setTodos(prev => [...prev, newTodo])

The same holds for objects: spread the old one into a new object with your change applied, and return that from the updater. Treat the value you get from useState as read-only, and always hand the setter something new.

Nesting needs care. Spreading an object copies its top level, so a nested object inside it is still the same object, and changing that nested one reaches back into the original:

jsx
// Mutates the original: the nested address object is still shared with the new one
user.address.city = 'Oslo'
setUser({ ...user })

// Works: a new object at every level down to the change
setUser(prev => ({
  ...prev,
  address: { ...prev.address, city: 'Oslo' }
}))

The parentheses around the object are what make the arrow function return it rather than read it as a block of code. Every level down to the change needs its own spread, which gets awkward as nesting deepens. When you find yourself writing a chain of them, that's usually a sign the state wants flattening into smaller pieces.

Changing one item in a list

Changing an item that's already in an array is where people most often get stuck. The tool for it is map. Walk the array, return a changed copy for the item you're after, and return every other item as it is:

jsx
function toggleTodo(id) {
  setTodos(prev => prev.map(todo =>
    todo.id === id ? { ...todo, done: !todo.done } : todo
  ))
}

The setter takes the updater form, since the next array is built from the current one. The ternary does the selecting: the matching todo gets a copy with done flipped, and everything else passes straight through. Those pass-through items are the same objects they always were, which is fine. Only the one you changed needs to be new, along with the array holding them, and map builds that fresh array for the setter.

Removing an item is filter, and adding one is the spread from the previous section. Between the three, most list updates are covered.

When the initial value is expensive

JavaScript evaluates the argument you pass to useState on every render, before useState itself runs, and React keeps the result only the first time. For a value like 0, that costs nothing. For a function call that does real work, it runs every render and the result goes unused after the first:

jsx
// buildBoard() runs on every render, and its result is discarded after the first
const [board, setBoard] = useState(buildBoard())

// buildBoard is called once, on the first render
const [board, setBoard] = useState(buildBoard)

The first line calls the function and hands React the result. The second hands React the function itself and lets React call it once. Save this for work that's genuinely costly, like reading from storage, generating a large structure, or a heavy computation. On useState(0) it's noise.

Version note

In class components, state lived in this.state and you updated it with this.setState. useState is the function-component equivalent, and it's the standard way to hold state in modern React.

A couple of practical habits save you trouble here. Reach for the updater form (setCount(c => c + 1)) by default whenever the next value builds on the current one, and keep the plain form (setCount(0)) for setting a fresh value that ignores the old one. That one rule sidesteps most timing surprises before you have to think about them.

Keep state minimal, too. Only store what you cannot work out from something you already have. If you hold a list of items in state, do not also keep their count in state; read it with items.length while rendering. Every extra piece of state is another value to keep in sync, and duplicated state is exactly where two copies drift apart. When several values always change together, a single object or a useReducer hook is often steadier than a scatter of separate useState calls.

It helps to think of state as a snapshot. For any single render, count is a fixed value, decided when that render started and unchanged for as long as it runs. setCount doesn't reassign that variable; it asks React to render again, and the new value only appears in the next render's snapshot. So reading state right after setting it gives you the old value:

jsx
function handleClick() {
  setCount(count + 1)
  console.log(count) // still the value from this render, not the new one
}

This is also where stale closures come from. Every function you define during a render, event handlers, effects, timers, closes over the state values from that render. If one of them runs later, a setTimeout callback for instance, it still sees the count it captured, even though the value has moved on since. The updater form sidesteps this, because setCount(c => c + 1) asks React for the current value at the moment it runs instead of relying on the captured one.

Batching is the other half of the picture. React groups the state updates that happen inside a single event handler and re-renders once, after the handler finishes, rather than after each setCount. That's why calling the setter twice with count + 1 still only moves the value by one: both calls queue against the same snapshot before any re-render happens. The updater functions, by contrast, are queued and run in order against each other, so they compose. Since React 18 this batching extends to updates inside promises, timeouts, and other async callbacks too, so the same rules apply well beyond plain event handlers.

JunoState is a component's memory A plain variable forgets its value every time the function runs, and useState remembers it. You get back the value and a setter, and calling that setter both stores the new value and redraws the component. When the new value builds on the old one, use setCount(c => c + 1) so you're always working from the latest. When that value is an array or object, give the setter a brand new one with your change applied.
JunoState is a component's memoryuseState gives you a value and a setter, and calling the setter is what triggers a re-render. Use the updater form when the next value depends on the current one or when you update several times in a row. State is per instance, so two of the same component keep separate values, and you always pass the setter a new value instead of mutating the old array or object. For a list, that means map: a new copy of the item you're changing, everything else untouched.
JunoState is a component's memory Treat state as a snapshot: fixed for the duration of a render, updated only on the next one, which is why reading it right after setting it gives the old value and why captured closures go stale. React batches the updates in an event and re-renders once, so setter calls against the same snapshot don't stack, while updater functions do. When correctness depends on the previous value, reach for the updater form, and when the starting value is expensive to build, hand useState the function itself so React runs it once.

Next up: Events, where those onClick handlers you've been writing get a proper look.