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 →