mosaddik.dev
All posts

React useState: 6 common mistakes and how to fix them

Mosaddik Billah 4 min read

JavaScript & React

A laptop showing React code with useState in a code editor
Photo by Rahul Mishra on Unsplash
On this page
  1. 1. Expecting the new value straight after setting it
  2. 2. Updating from the old value more than once
  3. 3. Changing an object or array directly
  4. 4. Copying props into state
  5. 5. Storing values you can calculate
  6. 6. Running slow code on every render
  7. A quick checklist

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#

JSX
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:

JSX
function handleClick() {
  const next = count + 1;
  setCount(next);
  console.log(next); // 1
}

2. Updating from the old value more than once#

JSX
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:

JSX
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#

JSX
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:

JSX
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#

JSX
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:

JSX
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#

JSX
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:

JSX
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#

JSX
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:

JSX
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