Don’t Always Depend on useEffect

When you use React, one of the most popular hooks is useEffect. A common reason we default to useEffect is because we want to update local state (useState) or make an asynchronous call to fetch/re-fetch data. Often we will see a combination of both, but in order to re-fetch or update, we might end up including the state or the asynchronous call in the dependency array. By doing so can lead to a pain-staking problem, called INFINITE re-rendering.

What’s the problem with useEffect?

The example below shows what happens when we use useEffect to fetch a random joke and update that result in the state, “mood”.


function Mood() {
  const [mood, setMood] = useState({
    myMood: "bored",
    date: new Date(),
    setupJoke: '',
    punchlineJoke: '',
  });

  useEffect(() => {
    const fetchData = async () => {
        return await fetch('https://official-joke-api.appspot.com/random_joke', {
            method: "GET",
            mode: 'cors',
        })
        .then(response => response.json())
        .then(data => {
            console.log(data);
            setMood({...mood, setupJoke: `${data.setup}`, punchlineJoke: `${data.punchline}`})
        });
    }
    fetchData();
  }, [mood.myMood])

  return (
JOKE: {` ${mood.setupJoke} — ${mood.punchlineJoke}`}

); } export default Mood;

If you end up trying this example out, you’ll notice that the random joke, rendering on the page, is constantly re-fetching to the point the page will break. Even if you remove mood from the dependency array, you will immediately see a yellow squiggly line in your IDE, suggesting:

“React Hook useEffect has a missing dependency: ‘mood’. Either include it or remove the dependency array. You can also do a functional update ‘setMood(m => …)’ if you only need ‘mood’ in the ‘setMood’ call.”

Naturally, you might end up reinserting mood and setMood, which forces you to return to the original problem of this infinite loop.

Not to mention, this suggestion is telling you to include a whole object, which is another problem. And now the compiler isn’t looking for updates to mood.setupJoke or mood.punchlineJoke (where the fetched data was updated into), but observing changes to the whole object mood. So whenever, mood.myMood or mood.date changes, then useEffect is called every time along with every second the data is re-fetched. To attempt to fix this problem, you would also try to put [mood.setupJoke, mood.punchlineJoke] in the dependency array, but this will ALSO cause an infinite loop.

What can we do?

Thankfully we have two hooks that can be better solutions to our problem. useCallback and useMemo.

`useCallback`

This hook caches the return value until one of the dependencies in the dependency array changes. Once the dependency changes, the return type of the function is re-evaluated.

`useMemo`

On the other hand, this hook caches the function. When, again, the dependency in the dependency array changes, the function is also re-evaluated.

What’s a potential solution and why?

useMemo(() => {
const fetchData = async () => {
return await fetch('https://official-joke-api.appspot.com/random_joke', {
method: "GET",
mode: 'cors',
})
.then(response => response.json())
.then(data => {
console.log(data);
setMood({...mood, joke: `${data.setup} --- ${data.punchline}`})
});
}
fetchData();
},[mood.myMood])

There are two things to notice here: useMemo and mood.myMood

I chose useMemo because it caches the return value of fetchData(). Only when mood.myMood updates, will this hook re-fetch the data to cache a new value. Because mood.myMood is a different property than the ones updated when the data is fetched, mood.setupJoke and mood.punchlineJoke remain cached with the previously fetched data. Finally, we’ve avoided infinite re-rendering! 🎉

So now you know. I hope you’ll think twice before using useEffect. Good luck!

Conversation

Join the conversation

Your email address will not be published. Required fields are marked *