Give components memory with useState, update it safely, and derive everything else from it.
Why it matters
Props let a parent configure a component, but interfaces also change on their own: a counter goes up, a menu opens, a tab is selected. State is the data a component remembers between renders. Modelling state well, keeping just the right data in it and deriving the rest, is the single biggest factor in whether React code stays simple.
Why a normal variable is not enough
function Counter() {
let count = 0;
return <button onClick={() => { count = count + 1; }}>Clicked {count}</button>;
}
Clicking changes the local variable, but nothing happens on screen. Two problems: React does not know it should re-render, and even if it did, count would start again at 0 because the function runs from the top each time.
useState
useState gives a component a value that survives re-renders, and a setter that tells React to render again:
function Counter() {
const [count, setCount] = React.useState(0);
return <button onClick={() => setCount(count + 1)}>Clicked {count}</button>;
}
React.useState(0)returns a pair: the current value and a function to replace it. Array destructuring names them. In projects with a build step you usuallyimport { useState } from "react"and writeuseState(0).- The argument is the initial value, used only on the first render.
- Calling
setCount(...)schedules a re-render. On the next rendercountholds the new value.
Each instance of a component has its own state. Two <Counter /> elements count independently.
State is a snapshot
Inside one render, count is a constant. Setting state does not change the variable immediately:
function handleClick() {
setCount(count + 1);
setCount(count + 1);
console.log(count); // still the old value
}
Both calls compute count + 1 from the same snapshot, so the count goes up by one, not two. When the next value depends on the previous one, pass an updater function:
setCount((c) => c + 1);
setCount((c) => c + 1); // now +2
React queues updaters and applies them in order. Using the updater form for "add", "toggle" and similar changes is a good habit.
Objects and arrays: replace, do not mutate
React decides whether to re-render by checking whether you gave it a new value. If you change an object in place and pass the same object back, React may skip the update, and other code holding that object sees surprising changes. Always create a new object or array:
const [user, setUser] = React.useState({ name: "Ada", city: "London" });
// Wrong: mutates the existing object
user.city = "Paris";
setUser(user);
// Right: copy, then change
setUser({ ...user, city: "Paris" });
const [todos, setTodos] = React.useState([]);
setTodos([...todos, newTodo]); // add
setTodos(todos.filter((t) => t.id !== id)); // remove
setTodos(todos.map((t) => (t.id === id ? { ...t, done: true } : t))); // update one
Keep the minimum state, derive the rest
Ask of every piece of data: does it change over time, and can it be computed from something else? If it can be computed, do not store it.
// Too much state: total can drift out of sync with items
const [items, setItems] = React.useState([]);
const [total, setTotal] = React.useState(0);
// Better: derive during render
const total = items.reduce((sum, i) => sum + i.price, 0);
The same goes for "is the form valid", "how many are done", "filtered list". Compute them from state each render. It is fast and can never be stale.
Disabling controls from state
Derived values are perfect for UI rules:
<button onClick={() => setCount((c) => c - 1)} disabled={count === 0}>
Subtract
</button>
The button's disabled attribute is computed from count, so it is always right.
The rules of hooks
useState is a hook: a function whose name starts with use and that hooks into React features. Hooks have two rules:
- Call them only at the top level of a component (or another hook), never inside
if, loops or nested functions. - Call them only from React components or custom hooks.
React matches hooks by call order, so the order must be the same on every render.
Common mistakes
- Expecting the state variable to change right after calling the setter.
- Mutating objects or arrays in state.
- Storing derived values (totals, filtered lists) in state.
- Calling
useStateinside a condition.
Recap
- State is per-instance memory that survives re-renders;
useState(initial)returns[value, setValue]. - Setting state schedules a new render; within a render the value is a fixed snapshot.
- Use updater functions for changes based on the previous value.
- Replace objects and arrays instead of mutating them, and derive anything you can compute.
Try it
Open the Do tab. You will turn a static counter into one that adds, subtracts and resets with useState, with a Subtract button that disables itself before going below zero.