Skip to main content

State and Reducers

useState Explained

Written by Published

A component function runs again on every render, so a plain variable resets every time. State is the box React keeps for you between those runs.

Why a normal variable fails

The component function re-runs from the top on each render. Any let inside it is created fresh, so an update made during one render is gone by the next - and nothing tells React to re-render in the first place.

Plain variable versus state

jsx

function BrokenCounter() {
  let count = 0                     // recreated on every render

  return (
    <button onClick={() => { count = count + 1 }}>
      Plain variable: {count} (never changes)
    </button>
  )
}

function WorkingCounter() {
  const [count, setCount] = React.useState(0)

  return (
    <button onClick={() => setCount(count + 1)}>
      State: {count}
    </button>
  )
}

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(
  <div style={{ display: "grid", gap: 8, justifyItems: "start" }}>
    <BrokenCounter />
    <WorkingCounter />
  </div>
)

The first button really does increment its variable - you just never see it, because nothing asks React to render again and the value is discarded when it does.

The two things you get back

useState returns an array of exactly two items, which is why the line is always written with array destructuring.

  1. The current value for this render.
  2. A setter that stores a new value and schedules a re-render.

The names are yours to choose, but the convention [thing, setThing] is universal and worth following - deviating from it makes React code read as unfamiliar for no gain.

The argument is the initial value only

The value passed to useState is used on the first render and ignored on every one after. This confuses people who pass a prop and expect the state to follow when the prop changes - it does not.

Initial means initial

jsx

function Field({ startWith }) {
  // Only read once. Changing startWith later will not update this.
  const [text, setText] = React.useState(startWith)

  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <p>Prop was: "{startWith}" — state is now: "{text}"</p>
    </div>
  )
}

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<Field startWith="hello" />)

State is per component instance

Render the same component twice and each copy gets its own state. They share code, not values - which is what makes a component reusable rather than a global.

Two instances, two counts

jsx

function Counter({ label }) {
  const [count, setCount] = React.useState(0)

  return (
    <button onClick={() => setCount(count + 1)}>
      {label}: {count}
    </button>
  )
}

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(
  <div style={{ display: "flex", gap: 8 }}>
    <Counter label="First" />
    <Counter label="Second" />
  </div>
)
  • State survives re-renders; a plain variable does not.
  • The setter is what schedules the re-render.
  • The argument is the initial value, read once.
  • Every instance of a component has its own state.