React2 min read

Delete the effect that copies props into state

Syncing state with useEffect gives you an extra render, a flash of stale data, and two sources of truth. Most of the time you can calculate the value during render instead.

This shape is everywhere:

function ProductList({ products, query }) {
  const [filtered, setFiltered] = useState([]);

  useEffect(() => {
    setFiltered(products.filter((p) => p.name.includes(query)));
  }, [products, query]);

  return <List items={filtered} />;
}

It works, mostly. It also renders twice for every change, shows the previous list for one frame, and gives you two things that can disagree.

The mental model

State is for things React cannot work out on its own: what the user typed, whether the panel is open, what came back from the server.

Anything you can calculate from props and state is not state. It is a value. Calculate it while rendering.

function ProductList({ products, query }) {
  const filtered = products.filter((p) => p.name.includes(query));
  return <List items={filtered} />;
}

One render. No stale frame. Nothing to keep in sync, because there is only one source of truth.

But is that slow?

Usually not. Filtering a few hundred items takes a fraction of a millisecond, far less than the render it is part of.

Measure before you optimise, and if it really is slow, cache it rather than storing it:

const filtered = useMemo(
  () => products.filter((p) => p.name.includes(query)),
  [products, query],
);

useMemo still runs during render, so the result is never out of date. It just skips the work when the inputs have not changed.

The reset case

The other common effect is resetting state when a prop changes:

useEffect(() => {
  setDraft("");
  setError(null);
}, [userId]);

There is a better tool: give the component a key. When the key changes, React unmounts the old component and mounts a fresh one, so all of its state starts over.

<ProfileForm key={userId} userId={userId} />

No effect, no partially reset state, and it cannot be forgotten when someone adds a fourth piece of state next year.

Adjusting state during render

Occasionally you need to change a piece of state when a prop changes, and only part of it. React supports setting state during render, as long as it is in the same component and guarded:

function List({ items }) {
  const [selection, setSelection] = useState(null);
  const [prevItems, setPrevItems] = useState(items);

  if (items !== prevItems) {
    setPrevItems(items);
    setSelection(null);
  }
  ...
}

React throws away the render in progress and re-runs immediately, before anything reaches the screen, so there is no flash. It is still a last resort, and the key approach is better when it fits.

When an effect is right

Keep the effect when you are talking to something outside React:

  • a subscription to a store, a socket, or an event
  • reading or writing the DOM directly, for example measuring an element
  • analytics on view
  • setting up a third party widget

The test I use: if removing the effect would only mean the screen shows something different, it was derived state. If removing it would leave a connection open or a listener attached, it is a real effect.

Written by Rahul Mourya, full stack engineer. Spotted a mistake or want the follow-up post? Email me.

All posts
  • React2 min read

    You pushed to the array and nothing rendered

    React compares state with Object.is, so changing an array or object in place looks like no change at all. Here are the copy patterns, including the one for nested data.

    • state
    • immutability
    • rendering
  • React2 min read

    One context is re-rendering your whole app

    Context has no selectors. Every consumer re-renders when the value changes, even the ones using a field that did not. Splitting the context fixes most of it.

    • context
    • performance
    • state