useState

Adds a local state variable to a function component and re-renders it whenever that value changes.

Signature
const [state, setState] = useState(initialState)
  • Added in React 16.8.0

Parameters

ParameterTypeDescription
$initialStateS | (() => S)The value the state should start as. It can be any type, but a function is treated specially: it is called once as an initializer function during the initial mount and its return value becomes the initial state. This argument is ignored after the first render.

Return value

[state: S, setState: (nextState: S | ((prev: S) => S)) => void]An array with exactly two values: the current state (equal to initialState on the first render), and the set function that updates the state and triggers a re-render. The set function itself returns nothing.

Errors

Calling it outside the top level of a component or Hook throws "Invalid hook call"; calling the set function unconditionally during render throws "Too many re-renders".

Changelog

  • PHP 18.0.0Updates are batched automatically everywhere, including inside promises, timeouts and native event handlers — not only inside React event handlers.
  • PHP 18.0.0Strict Mode calls the initializer and updater functions twice in development to surface impurities; one result is discarded.

useState is the default choice for any value a component owns and re-renders on: a form field, an open/closed flag, a fetched result, a counter. Each call returns the current value and a setter; calling the setter schedules a re-render with the new value.

When to reach for it

  • The value is owned by one component and its children read it through props.
  • The next value is simple to compute at the call site.
  • You have fewer than roughly four related pieces of state — past that, useReducer keeps the transitions honest.

What trips people up

The setter does not mutate the variable you already have in scope. After setCount(count + 1), count in the same function still holds the old number until the next render. When the next value depends on the previous one — especially with two updates in one handler — pass an updater function instead: setCount(c => c + 1). React queues updaters and applies them in order.

Passing a function as initialState makes it an initializer, not a state value. To actually store a function, wrap it: useState(() => myFn). And because React compares with Object.is, mutating an object or array in place and setting the same reference back skips the re-render entirely — always set a new object.

Example

Try it yourself
Loading editor…
Loading React…

When to use it

  • Track the value of a controlled text input or select in a form.
  • Toggle a modal, accordion or dropdown open and closed.
  • Hold a list of items that buttons in the same component add to or remove from.
  • Store a step index while walking a user through a multi-step wizard.

Frequently asked questions

Why is my state one step behind after calling the setter?

Because the setter only schedules the value for the next render — the variable in the current function keeps its old value until React re-renders. If you need the new value inside the same handler, compute it into a local constant and use that, and use the updater form setX(prev => ...) whenever the next value depends on the previous one.

How do I update an object or array in state?

Create a new object or array rather than mutating the existing one, e.g. setUser(prev => ({ ...prev, name })) or setItems(prev => [...prev, item]). React compares the old and new state with Object.is, so pushing onto the same array leaves the reference unchanged and the component never re-renders.

How do I store a function in state?

Wrap it in another function: useState(() => myCallback). A bare function argument is interpreted as a lazy initializer and React calls it to produce the initial state, so useState(myCallback) stores whatever myCallback returns instead of the function itself. The same applies to the setter: setFn(() => nextFn).

More examples

Lazy initial state

buildLookup runs exactly once. Typing in the input re-renders the component but does not rebuild the map, because the initializer argument is ignored after the first render. Typing "gamma" shows "length: 5"; typing "delta" shows "length: unknown word".

Example · jsx
import { useState } from "react";

function buildLookup(words) {
  // Pretend this is expensive. Without the arrow wrapper below it would
  // run on every single render, not just the first one.
  const map = {};
  for (const word of words) {
    map[word] = word.length;
  }
  return map;
}

const WORDS = ["alpha", "beta", "gamma"];

export default function Lengths() {
  // Passing the function itself makes React call it once, on mount.
  const [lookup] = useState(() => buildLookup(WORDS));
  const [query, setQuery] = useState("beta");

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <p>
        length: {lookup[query] ?? "unknown word"}
      </p>
    </div>
  );
}

Updating objects and arrays without mutating

The list starts at "2 remaining". Each checkbox click produces a fresh array and a fresh object for the toggled row, so Object.is sees a change and the count updates. Replacing the map call with an in-place assignment would leave the screen frozen at 2.

Example · jsx
import { useState } from "react";

const INITIAL = [
  { id: 1, label: "Write migration", done: true },
  { id: 2, label: "Review PR", done: false },
  { id: 3, label: "Ship release", done: false },
];

export default function TodoList() {
  const [todos, setTodos] = useState(INITIAL);

  function toggle(id) {
    // New array, new object for the changed row. Mutating
    // `todo.done = !todo.done` would keep the same reference
    // and React would skip the re-render.
    setTodos((prev) =>
      prev.map((todo) =>
        todo.id === id ? { ...todo, done: !todo.done } : todo
      )
    );
  }

  const remaining = todos.filter((t) => !t.done).length;

  return (
    <div>
      <p>{remaining} remaining</p>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <label>
              <input
                type="checkbox"
                checked={todo.done}
                onChange={() => toggle(todo.id)}
              />{" "}
              {todo.done ? <s>{todo.label}</s> : todo.label}
            </label>
          </li>
        ))}
      </ul>
    </div>
  );
}

Resetting child state with a key

Type a draft, press "Next thread", and the textarea is empty again: the key change gave CommentBox a new identity, so its useState re-initialised. Remove the key and the half-written draft follows you to the next author, which is the usual source of "my form keeps the previous record's values" bugs.

Example · jsx
import { useState } from "react";

function CommentBox({ author }) {
  const [draft, setDraft] = useState("");
  return (
    <div>
      <p>Replying to {author}</p>
      <textarea
        value={draft}
        onChange={(e) => setDraft(e.target.value)}
        placeholder="Write a reply"
      />
      <p>{draft.length} characters</p>
    </div>
  );
}

const AUTHORS = ["Ada", "Grace", "Alan"];

export default function Inbox() {
  const [index, setIndex] = useState(0);
  const author = AUTHORS[index];

  return (
    <div>
      <button onClick={() => setIndex((i) => (i + 1) % AUTHORS.length)}>
        Next thread
      </button>
      {/* Changing the key unmounts the old CommentBox and mounts a fresh
          one, so `draft` starts empty again for each author. */}
      <CommentBox key={author} author={author} />
    </div>
  );
}

Discussion

  • Be the first to comment on this lesson.