Fix useEffect re-running on every render

By Dave Ceddia

A friend of mine had this problem.

Basically, every time my list changes, my websocket has to reconnect.

He had a call to useEffect that was setting up a websocket connection, and tracking some items in a list, something like this snippet:

const [list, setList] = useState([]);

const showLoading = useCallback((id, loading) => {
  // Update the list
  setList(list.map(item => {
    if(item.id === id) {
      return {
        ...item,
        isLoading: loading
      }
    }
    return item;
  }));
}, [list]);

useEffect(() => {
  // Connect the websocket
  const ws = new Websocket(...);

  // A message signals to reload one of the `list` items
  ws.addEventListener("message", e => {
    showLoading(e.id, true);
  });

  // Before next effect runs, close this websocket
  return () => ws.close();
}, [showLoading]);

Now, this isn’t a complete component; in the full code there’s probably something that loads data for the list items, and surely some content to be returned. That would only distract from the bug, though.

The problem is between the effect and the callback, and specifically, it’s a problem with their dependencies.

The chain of events goes like this:

  • The component mounts, runs the effect, and connects the websocket. Great.
  • Later, a message comes in. The handler calls showLoading. Fine.
  • showLoading calls setList, which modifies the list. Okay.
  • Now, anything that depends on list must be recomputed… (uh oh)
    • the useCallback function, showLoading, depends on [list]. So it gets recreated.
    • the useEffect depends on [showLoading]… so now that gets recreated too.

So it ends up that every message triggers the effect to re-run, which disconnects and reconnects the websocket.

console.log to the rescue

If you had code like this, inserting console.logs in key places would help with understanding the timing. I’d probably sprinkle them around like this:

const [list, setList] = useState([]);

console.log('[render]');

const showLoading = useCallback((id, loading) => {
  console.log('[showLoading]', id, loading);
  // Update the list
  setList(list.map(item => {
    if(item.id === id) {
      return {
        ...item,
        isLoading: loading
      }
    }
    return item;
  }));
}, [list]);

useEffect(() => {
  console.log('[useEffect] setup');
  // Connect the websocket
  const ws = new Websocket(...);

  // A message signals to reload one of the `list` items
  ws.addEventListener("message", e => {
    console.log('[useEffect] websocket message!');
    showLoading(e.id, true);
  });

  // Before next effect runs, close this websocket
  return () => {
    ws.close();
    console.log('[useEffect] cleanup');
  }
}, [showLoading]);

I’d expect to see it print something like this:

[render]
[useEffect] setup
[useEffect] websocket message!
[showLoading] 1 true
[render]
[useEffect] cleanup
[useEffect] setup

You might see some extra renders in dev mode. Sometimes React calls components twice in dev mode, but that shouldn’t happen in the production build.

Fix useEffect Running Too Often

We need to break the chain somehow. The effect depends on showLoading, and showLoading depends on the list – ipso facto, the effect depends on the list.

Ultimately, the effect needs to depend on less stuff.

Here is a perfect place for functional setState. Have a look:

const [list, setList] = useState([]);

// Normal setState:
setList(list.map(...))

// Functional setState:
setList(oldList => oldList.map(...))

The functional or “updater” form of the state setter will take a function, and pass the current state value to that function. Whatever the function returns becomes the new state value.

This is great because it means we don’t need to depend on list to be able to update the list!

Here’s our functional setState fix applied:

const [list, setList] = useState([]);

const showLoading = useCallback((id, loading) => {
  // Update the list
  setList(currentList => currentList.map(item => {
    if(item.id === id) {
      return {
        ...item,
        isLoading: loading
      }
    }
    return item;
  }));
}, []); // <-- depends on nothing, now

useEffect(() => {
  // Connect the websocket
  const ws = new Websocket(...);

  // A message signals to reload one of the `list` items
  ws.addEventListener("message", e => {
    showLoading(e.id, true);
  });

  // Before next effect runs, close this websocket
  return () => ws.close();
}, [showLoading]); // <-- showLoading will never change

That change fixes the problem. Now, when websocket messages come in, showLoading will be called, the list will be updated, and the effect won’t run again.

Without the right mental model, useEffect is super confusing.
With the right mental model, you'll sidestep the infinite loops and dependency warnings before they happen.
Get great at useEffect this afternoon with Learn useEffect Over Lunch.

Move Callbacks Inside The useEffect

You might run into something like this, where the useEffect needs to call a function from the surrounding scope:

const doStuff = () => { ... }

useEffect(() => {
  if(whatever) {
    doStuff();
  }
}, [whatever, doStuff]);

This isn’t great, because the effect will re-run on every render.

Remember that React calls your component function on every render, and const doStuff here is just a local variable – a variable that will go out of scope when the function ends, and be recreated the next time around.

To make this effect run less often, move the callback inside the effect:

useEffect(() => {
  const doStuff = () => { ... }

  if(whatever) {
    doStuff();
  }
}, [whatever]);

Further Reading on useEffect

React’s useEffect hook can feel like a magical incantation sometimes. Mostly, it’s that dependency array.

With no array at all, your effect function will run every render.

With an empty array [], the effect will run only once.

With variables in the array, like [a, b], the effect will run only when a or b change.

These variables can be state or one or more props or anything else you need.

Check out my post Examples of the useEffect Hook for more depth and more examples.