Skip to content
React rc hooks 4 min read

useState

useState is the foundational Hook for adding reactive state to a function component. It lets a component “remember” a value between renders and tells React to re-render whenever that value changes. Almost every interactive component you write—toggles, forms, counters, fetched data—depends on it. This page walks through declaring state, reading it, the setter function, lazy initialization, functional updates, working with objects and arrays, and resetting state cleanly.

Declaring and reading state

useState takes the initial state as its argument and returns an array with exactly two elements: the current value and a setter function. Array destructuring gives them readable names.

import { useState } from "react";

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

  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  );
}

On the first render, count is 0 (the initial value). Each click calls setCount, React schedules a re-render, and on the next render count holds the new value. The convention is to name the pair [thing, setThing].

Reading state is always synchronous and reflects the value for the current render. The variable does not “live update” mid-render—React hands you a fresh snapshot each time the component runs.

The setter function

The setter (setCount above) does two things: it stores the next value and schedules a re-render. It does not mutate the existing variable—calling it does not change count in the line right after.

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

  function handleClick() {
    setCount(count + 1);
    console.log(count); // still the old value (0) in this render
  }

  return <button onClick={handleClick}>{count}</button>;
}

Output:

0   // logged on first click, even though state will become 1

This is because count is a constant captured by the current render. The new value only appears on the next render.

Functional updates

When the next state depends on the previous state—especially across multiple updates in one event—pass a function to the setter. React calls it with the latest pending state and uses the return value.

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

  function addThree() {
    setCount((c) => c + 1);
    setCount((c) => c + 1);
    setCount((c) => c + 1);
  }

  return <button onClick={addThree}>{count} (+3)</button>;
}

Output:

// After one click, count = 3

If you wrote setCount(count + 1) three times instead, all three would read the same stale count and the result would be 1. The functional form (c) => c + 1 is the safe pattern whenever updates batch or queue.

Lazy initial state

The argument to useState is only used on the first render, but it is still evaluated on every render. If computing the initial value is expensive, pass an initializer function instead—React calls it once.

function TodoList() {
  // ✅ runs only on the initial render
  const [todos, setTodos] = useState(() => loadFromStorage());
  return <ul>{todos.map((t) => <li key={t.id}>{t.text}</li>)}</ul>;
}

function loadFromStorage() {
  return JSON.parse(localStorage.getItem("todos") ?? "[]");
}

Note: pass loadFromStorage (a function reference), not loadFromStorage() (its result), or you lose the lazy benefit.

Object and array state

State updates replace the value rather than merging it. To update one field of an object, spread the old object and override the changed key—never mutate it in place.

function ProfileForm() {
  const [form, setForm] = useState({ name: "", email: "" });

  function updateField(e) {
    const { name, value } = e.target;
    setForm((prev) => ({ ...prev, [name]: value }));
  }

  return (
    <form>
      <input name="name" value={form.name} onChange={updateField} />
      <input name="email" value={form.email} onChange={updateField} />
    </form>
  );
}

Arrays follow the same rule—produce a new array instead of calling push, splice, or assigning by index.

setItems((prev) => [...prev, newItem]);          // add
setItems((prev) => prev.filter((x) => x.id !== id)); // remove
setItems((prev) => prev.map((x) =>               // update one
  x.id === id ? { ...x, done: true } : x
));

Mutating state directly (form.name = value) skips React’s change detection. Because the object reference is unchanged, React may bail out of the re-render and the UI goes stale.

Resetting state with a key

Sometimes you want to throw away all state and reinitialize a component—for example, switching to edit a different record. Changing the key prop tells React to unmount the old instance and mount a fresh one, resetting every useState inside it.

function ProfileEditor({ userId }) {
  // A new userId remounts EditForm, resetting its internal state
  return <EditForm key={userId} userId={userId} />;
}

This is cleaner than manually resetting each piece of state inside an effect.

When state changes are equal

If you call the setter with a value that is Object.is-equal to the current state, React skips the re-render. This makes redundant updates cheap but is also why mutating an object and passing the same reference does nothing.

PatternResult
setCount(count) (same value)No re-render (bailed out)
setForm({ ...form }) (new ref, same data)Re-renders (new reference)
form.x = 1; setForm(form) (mutation)No re-render (same reference)

Best Practices

  • Treat state as immutable: always create new objects/arrays instead of mutating, so React detects the change.
  • Use the functional updater set((prev) => …) whenever the next value depends on the previous one.
  • Pass an initializer function for expensive initial values to avoid recomputing on every render.
  • Keep state minimal—derive values during render rather than storing them; only store what can’t be computed.
  • Split unrelated values into separate useState calls; group only fields that change together.
  • Reach for the key prop to reset a subtree instead of hand-resetting many state variables.
Last updated June 14, 2026
Was this helpful?