State with useState

React · Essentials · 15 min

State is what makes a UI interactive; misunderstanding when it updates is the most common React bug.

Regular variables don't survive re-renders, and changing them doesn't tell React to update. For data that changes over time, use state:

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

useState returns the current value and a setter. Calling the setter stores the new value and schedules a re-render; on that render useState returns the new value. Each component instance has its own state: render <Counter /> twice and you get two independent counts.

useState is a Hook. Hooks are functions starting with use, and they must be called at the top level of a component (not inside conditions, loops or nested functions), because React matches them up by call order.

The key mental model: state is a snapshot. During one render, count is a constant. Setting it doesn't change the variable you already have, it requests a new render:

setCount(count + 1);
setCount(count + 1);   // still count + 1: both read the same snapshot
console.log(count);    // old value

React also batches updates from the same event into one re-render. When the next value depends on the previous one, pass an updater function, which React applies to the latest queued state:

setCount(c => c + 1);
setCount(c => c + 1);  // +2

If initial state is expensive to compute, pass a function: useState(() => buildIndex(data)) runs only on the first render.

Only store what can't be computed. If a value can be derived from props or other state (a full name from first and last, a filtered list), compute it during render instead of keeping it in sync in state.

Example

import { useState } from "react";

function Counter({ step = 1 }) {
  const [count, setCount] = useState(0);

  function addThree() {
    // updater functions see the latest pending value
    setCount(c => c + step);
    setCount(c => c + step);
    setCount(c => c + step);
    console.log(count); // still the old snapshot
  }

  const parity = count % 2 === 0 ? "even" : "odd"; // derived, not state

  return (
    <div>
      <p>{count} ({parity})</p>
      <button onClick={() => setCount(count + step)}>+{step}</button>
      <button onClick={addThree}>+{3 * step}</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

export default function App() {
  return <><Counter /><Counter step={10} /></>;
}

Remember

  • const [v, setV] = useState(init); calling the setter schedules a re-render
  • State is a snapshot: the variable doesn't change until the next render
  • Use setV(prev => next) when the new value depends on the old one
  • Call Hooks only at the top level; derive values instead of duplicating them in state

Watch out for

  • Reading state right after setting it and expecting the new value.
  • Copying props into state and then wondering why it doesn't update when the prop changes.

Check yourself

  1. count is 0. A click handler runs setCount(count + 1); setCount(count + 1); setCount(c => c + 1). What is count after the re-render?
  2. Why must Hooks not be called inside an if statement?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: react.dev/reference/react/useState