Hirefullstack – Software Engineering & IT-Beratung aus Berlin
← Back to overview
Beginner 12 min read

useState

Remembering something that is allowed to change

In one sentence

useState gives your component a memory: a value that survives the passes, plus a button that changes it and makes React redraw.

This is the hook you will use most often. Anything meant to change on screen sits in a useState.

shape.ts
const [value, setValue] = useState(initial);
//     ▲       ▲                ▲
//     |       |                only used on the very first pass
//     |       changes the value and makes React redraw
//     the value for this pass
useState returns an array with exactly two things; the square brackets on the left unpack it. You choose the names.
Put another way

An ordinary variable inside a component is a note thrown away after every pass. useState is the notebook React keeps for you – and the moment you write in it, React redraws the picture.

What you need it for

Reach for it when …
  • +

    Text in an input field

  • +

    Open/closed, on/off, the selected tab

  • +

    Counters, selections, loaded data

  • +

    In short: anything that changes and is visible

Skip it when …
  • Values you can compute from other state – just compute them

  • Things nobody sees (a timer id, say) → that is what RefA small note React keeps between renders – changing it does not redraw anything. is for

  • Simply copying a prop – the copy goes stale sooner or later

Two ways to set it

setting.tsx
// 1) Directly – when the new value does not depend on the old one
setName("Ada");
setOpen(false);

// 2) As a function – when the new value builds on the old one
setCount((c) => c + 1);
setItems((prev) => [...prev, item]);
Tip The rule of thumb

If the old value appears on the right (count + 1, [...items, x]), use the function form. It always gets the most recent state – even when you set several times in a row or the call happens later.

Never change the old thing – replace it

Updating objects and lists
nothing happens
user.name = "Ada";
setUser(user);

todos.push(item);
setTodos(todos);
this works
setUser({ ...user, name: "Ada" });

setTodos((prev) => [...prev, item]);

React only checks whether it is the same object as before. Change it on the inside and it is still the same one – so React does not redraw. Remember: copy, change, set.

patterns.ts
// add
setItems((p) => [...p, item]);

// remove
setItems((p) => p.filter((i) => i.id !== id));

// change one of them
setItems((p) => p.map((i) => (i.id === id ? { ...i, done: true } : i)));

// change one field in an object
setForm((f) => ({ ...f, email: "a@b.com" }));
You will need these four lines constantly. Worth knowing them by heart.
Pitfall Don't forget the rest of the object

setForm({ email: v }) replaces the whole object – the name is gone afterwards. Unlike the old class components, React does not merge anything for you. So always write ...f first.

Don't store what you can compute

two truths that drift apart
const [items, setItems] = useState([]);
const [count, setCount] = useState(0);

function add(x) {
  setItems([...items, x]);
  setCount(count + 1);   // heaven help you when someone forgets this on delete
}
one truth
const [items, setItems] = useState([]);

const count = items.length;   // always correct

function add(x) {
  setItems((p) => [...p, x]);
}

Ask yourself with every new piece of state: can I compute this from something else? If yes, it is not state.

In more depth: expensive initial values, functions in state, batching optional

Computing the starting value only once

The starting value is ignored from the second pass onwards – but the computation still runs every time if you write it directly. Hand over a function instead and React calls it only on the first pass.

lazy.tsx
// ❌ reads from storage on EVERY pass
const [form, setForm] = useState(JSON.parse(localStorage.getItem("f") ?? "{}"));

// ✅ reads only the first time
const [form, setForm] = useState(() => JSON.parse(localStorage.getItem("f") ?? "{}"));

When you really want to store a function

React reads setState(myFunction) as “here is the function form” and calls it. To store the function itself you need one more layer: setState(() => myFunction).

Why several setState calls redraw only once

React collects all the changes from one event and redraws once afterwards – this is called BatchingReact collects several changes and redraws once afterwards, instead of after every single one.. Since React 18 it also applies inside setTimeout and promises. So right after setCount(5), count still holds the old value.

batching.tsx
function handleClick() {
  setA(1);
  setB(2);
  setC(3);
  // React redraws ONCE, not three times
}

// If you need the new value straight away, compute it separately:
function handleClick2() {
  const next = count + 1;
  setCount(next);
  sendToServer(next);   // don't use count – it is still the old one
}

Does it stick?

5 questions on this lesson. Wrong answers show up in your stats.

Now write it yourself

Counter with a double step

Finish the counter. The +2 button is the interesting one: it should call setCount twice and really go up by 2 – the naive route only manages 1.

  • +1 increases the display by 1
  • +2 (twice +1) increases by 2 – using two setCount calls
  • Reset sets it back to 0
import { useState } from "react";

export default function App() {
  const [count, setCount] = useState(0);

  function plusOne() {
    // TODO
  }

  function plusTwo() {
    // TODO: call setCount twice – the result must be +2
  }

  function reset() {
    // TODO
  }

  return (
    <div style={{ fontFamily: "system-ui", padding: 16 }}>
      <p data-testid="count" style={{ fontSize: 32 }}>{count}</p>
      <button onClick={plusOne}>+1</button>{" "}
      <button onClick={plusTwo}>+2 (twice +1)</button>{" "}
      <button onClick={reset}>Reset</button>
    </div>
  );
}

Todo list

Finish the three functions. Important: never change the existing array, always build a new one.

  • Add appends the text and clears the input
  • Clicking an entry strikes it through
  • × removes the entry
import { useState } from "react";

type Todo = { id: number; text: string; done: boolean };

export default function App() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [text, setText] = useState("");

  function add() {
    if (!text.trim()) return;
    // TODO: append a new todo and clear the input
  }

  function toggle(id: number) {
    // TODO: flip done
  }

  function remove(id: number) {
    // TODO: remove the entry
  }

  return (
    <div style={{ fontFamily: "system-ui", padding: 16 }}>
      <input
        aria-label="todo"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="What needs doing?"
      />{" "}
      <button onClick={add}>Add</button>
      <ul>
        {todos.map((t) => (
          <li key={t.id}>
            <span
              onClick={() => toggle(t.id)}
              style={{ textDecoration: t.done ? "line-through" : "none", cursor: "pointer" }}
            >
              {t.text}
            </span>{" "}
            <button aria-label={"delete-" + t.text} onClick={() => remove(t.id)}>×</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Hirefullstack

Need React firepower on your team?

We have been building React and Next.js applications for clients across Germany for years – as a single expert, as reinforcement for an existing team, or as a complete Scrum team.

Talk about your project →