React useState: 6 common mistakes and how to fix them
Mosaddik Billah 4 min read

On this page
useState looks simple: a value and a function to change it. Most bugs with it come from the same handful of misunderstandings about when state changes and how React notices the change. Here are the six I see most often, with a fix for each.
1. Expecting the new value straight after setting it#
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
console.log(count); // still 0
}
return <button onClick={handleClick}>{count}</button>;
}Calling setCount doesn’t change count in the code that’s already running. It asks React to render the component again, and the next render gets the new value. Inside this click handler, count is fixed at what it was when the render happened.
Fix: if you need the new value right away, calculate it first and use that:
function handleClick() {
const next = count + 1;
setCount(next);
console.log(next); // 1
}2. Updating from the old value more than once#
function addThree() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}This adds 1, not 3, because all three lines read the same count. The same problem appears when an update runs later, in a timer or after an await, and uses a value from an earlier render.
Fix: pass a function. React calls it with the latest value:
function addThree() {
setCount((c) => c + 1);
setCount((c) => c + 1);
setCount((c) => c + 1);
}Use the function form whenever the new state depends on the old state.
3. Changing an object or array directly#
const [cart, setCart] = useState([]);
function addItem(item) {
cart.push(item);
setCart(cart); // nothing happens on screen
}React compares the old and new state with Object.is. cart is still the same array, so React decides nothing changed and may skip the render.
Fix: create a new array or object:
function addItem(item) {
setCart((items) => [...items, item]);
}
function removeItem(id) {
setCart((items) => items.filter((item) => item.id !== id));
}
function updateName(name) {
setUser((user) => ({ ...user, name }));
}The spread syntax and filter and map all return new arrays and objects, which is why they show up so often in React code.
4. Copying props into state#
function Price({ amount }) {
const [price, setPrice] = useState(amount);
return <p>{price}</p>;
}The value passed to useState is only used on the first render. When the parent later passes a new amount, price keeps the old one.
Fix: if you don’t need to edit the value, don’t put it in state. Use the prop directly, or calculate from it during render:
function Price({ amount }) {
const formatted = amount.toLocaleString("en-US", { style: "currency", currency: "USD" });
return <p>{formatted}</p>;
}If the component really does need its own editable copy, for example a form that starts from saved data, give it a key so React starts fresh when the data changes: <EditForm key={user.id} user={user} />.
5. Storing values you can calculate#
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);Now every change to items must also update total, and when one is forgotten they disagree.
Fix: keep only the source data in state and work out the rest while rendering:
const [items, setItems] = useState([]);
const total = items.reduce((sum, item) => sum + item.price, 0);The same goes for filtered lists, counts and anything else derived from existing state. This also removes many useEffect calls that only exist to keep two states in sync, a pattern covered in useEffect explained.
6. Running slow code on every render#
const [rows, setRows] = useState(buildRowsFromData(data));buildRowsFromData(data) runs on every render, even though React only uses the result the first time.
Fix: pass the function itself, without calling it. React calls it once:
const [rows, setRows] = useState(() => buildRowsFromData(data));This only matters for work that is actually slow, such as parsing a large dataset or reading from localStorage. For useState(0) or useState([]), the plain form is fine.
A quick checklist#
- Need the new value now? Calculate it in a variable first.
- New state depends on old state? Use
setX((old) => ...). - Objects and arrays: always create new ones, never change them in place.
- Can it be calculated from props or other state? Then it isn’t state.
- Slow starting value? Pass a function to
useState.
I build React interfaces for Wix sites as part of Wix CLI apps, with dashboard pages and site widgets written in React and TypeScript.
Comments