Avoiding recursive useEffect hooks in React

No Comments

It’s fair to say that React 16.8 and the introduction of
hooks has really changed how we
write React. Hooks are one of those APIs that make you realise the flaws of the
previous approach after you stop using it. I remember being very skeptical of
hooks when they were first released, not thinking that the previous class based
design had many flaws, but I’ve since come to realise I was very wrong, and
hooks are a vast improvement on how we build React components. If you’re
interested in comparing the old vs the new, I wrote a
blog post refactoring a component to use hooks
that offers a nice comparison.

One area that has taken me some time to get used to is the dependency array of
the useEffect hook. This lets you tell React when it should rerun the effect:

useEffect(
  () => {
    console.log('I run when `a` changes')
  },
  [a]
)

This useEffect will be run:

  • when the component is first mounted
  • whenever the variable a changes.

But this lead me to quite often end up with recursive calls to setEffect,
where I’d need to rely on some state in order to update its value:

const [count, setCount] = useState(0)

// this is going to go on forever
// because the effect relies on the `count` variable
// and then updates the `count` variable
// which triggers the effect
// and so on...
useEffect(
  () => {
    setCount(count + 1)
  },
  [count]
)

This is a contrived example for the purpose of demonstration, but I also had
bigger examples where we had an object in state with many keys and values, and
we needed to read in the object and update one part of it:

const [userData, setUserData] = useState({
  name: 'Jack',
  friends: ['alice', 'bob'],
})

// also runs infinitely for the same reasons as above
useEffect(
  () => {
    const newUser = {
      ...userData,
      friends: [...userData.friends, 'charlie'],
    }

    setUserData(newUser)
  },
  [userData]
)

The solution lies in how we call the set state functions (in the prior code
example, setUserData is the “set state” function). There are two forms to
these functions:

setUserData(newUser)
setUserData(function(oldUser) {
  const newUser = {}
  return newUser
})

The first takes the new value and sets it. The second takes a function that is
called with the old value
and is expected to return the new value. Let’s take
the previous useEffect code example and update it to use the second form of
the set state function:

const [userData, setUserData] = useState({
  name: 'Jack',
  friends: ['alice', 'bob'],
})

// doesn't run infinitely! 👌
useEffect(() => {
  setUserData(oldUser => {
    const newUser = {
      ...oldUser,
      friends: [...oldUser.friends, 'charlie'],
    }
    return newUser
  })
}, [])

Do you notice what’s different here? We no longer have to depend on userData,
because we read it from the callback function that we give to the set state
function! This means that our useEffect call is free to modify and set the new
user data without fear of recursion because it reads the old value by being
given it via the set state function. Therefore we can lose it from our
useEffect dependencies array, meaning that useEffect won’t rerun when it
changes!

My experience of this was that once I spotted this trick it made the useEffect
hook really click in my head. I’ve come to use the set state function variant
much more frequently – in fact, nearly exclusively inside useEffect calls, and
I recommend giving it a go.

    About us and this blog

    We are a digital marketing company with a focus on helping our customers achieve great results across several key areas.

    Request a free quote

    We offer professional SEO services that help websites increase their organic search score drastically in order to compete for the highest rankings even when it comes to highly competitive keywords.

    Subscribe to our newsletter!

    More from our blog

    See all posts

    Leave a Comment