useState
Remembering something that is allowed to change
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.
const [value, setValue] = useState(initial);
// ▲ ▲ ▲
// | | only used on the very first pass
// | changes the value and makes React redraw
// the value for this pass 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
- +
Text in an input field
- +
Open/closed, on/off, the selected tab
- +
Counters, selections, loaded data
- +
In short: anything that changes and is visible
- −
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
// 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]); 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
user.name = "Ada";
setUser(user);
todos.push(item);
setTodos(todos); 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.
// 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" })); 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
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
} 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.
// ❌ 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.
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
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.
- □
+1increases the display by 1 - □
+2 (twice +1)increases by 2 – using two setCount calls - □
Resetsets 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> ); }
Finish the three functions. Important: never change the existing array, always build a new one.
- □
Addappends 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> ); }