DEV Community

UponTheSky
UponTheSky

Posted on

The horror of useEffect

Well, I am recently making my personal portfolio to re-challenge the job market, and I had no choice but to choose React to make my portfolio's frontend part(Sorry, I am not a FE engineer so I know almost nothing about those "hot" ones like Next.js or Remix). Right, I am making my portfolio with "Vanilla" React which is not common nowadays.

However, while building logics of fetching data from the backend using useEffect, I heard a weird hissing sound from my old friend Intel Macbook Pro.

Image description

Wow, I must have done something wrong. But from where does this horrible bug come out? After a bit long thought, I finally could figure out that the reason was useEffect.

Observation

Let me explain this with a simple code.

First of all, here is a simple React component fetching data from an external API. Here we'll use JSON Placeholder for simplicity.

(I prefer TypeScript but the pure JS version would have no difference in its logic)

import React, { useState, useEffect } from 'react';
import axios from 'axios';

interface User {
  userId: number,
  id: number,
  title: string,
  completed: boolean
}

const API_URL = 'https://jsonplaceholder.typicode.com/todos/1';

export function App() {
  const [userInfo, setUserInfo] = useState<User | null>(null);

  useEffect(() => {
    const fetchUserInfo = async () => {
      try {
        const response = await axios.get<User>(API_URL);
        setUserInfo(response.data);
      } catch(error) {
        if (error instanceof Error) {
          console.error(error.message);
        }
      }
    };

    fetchUserInfo();
  }, [userInfo])

  if (!userInfo) {
    return <h1>Loading...</h1>
  }

  return (
    <div>
      user Id: {userInfo.userId} title: {userInfo.title} completed: {userInfo.completed}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Image description

Okay, this is a very simple application rendering a piece of data we fetched from an external API, and it works fine. So what is the matter?

Let's look into the useEffect part, which is our topic of interest. To see what's happening when we fetch the data, we simply add console.log within the fetchUserInfo function

useEffect(() => {
    const fetchUserInfo = async () => {
      console.log("here is where our bug could show up");
      try {
        const response = await axios.get<User>(API_URL);
        setUserInfo(response.data);
      } catch(error) {
        if (error instanceof Error) {
          console.error(error.message);
        }
      }
    };

    fetchUserInfo();
  }, [userInfo]) // => notice that we have our dependency array filled with "userInfo" state managed by useState
Enter fullscreen mode Exit fullscreen mode

Now save the code and see what's happening on the console tap of Chrome:

Image description

A-ha. Now we see the problem. So our useEffect keeps running its enrolled callback over and over again. Why is this bug happening?

The Source of the Bug?

If you read official docs on useEffect, you'll find that a callback enrolled into useEffect is invoked in the following mechanism(in simple words):

  • after the component renders
  • if an element in the dependency array(the second argument of useEffect) changes - if it is empty([]), there is no element to be referred by React, so React skips useEffect

Hence our senario of component rendering is like this:

Image description

  1. App component renders.
  2. useEffect invokes fetchUserInfo, which updates userInfo state.
  3. Since the component's state has changed, App will re-render.
  4. Now React looks up the dependency array passed to useEffect to determine whether it should re-invoke fetchUserInfo or skip it. However, since we have our dependency array as [userInfo], and userInfo has changed from null to an object, fetchUserInfo gets re-invoked.
  5. userInfo state is "updated" again.
  6. So like in step 3, App re-renders again
  7. Now the actual game begins: at step 5, we have our userInfo changed from null to a JS object. However, here we have two identical objects. So in our intention the component should not be re-rendered. However, since React compares the elements in the dependency array with Object.is method, React recognizes those two identical objects as different objects and determines that the component should re-render.
  8. We then basically go back to step 6 and repeat the process recursively.

Practical Suggestions

Now everything is clear. Although the server data doesn't change at all, React finds fetched data to be different from the previous one because of its comparison mechanism used for the dependency array of useEffect. But then, how do we solve our problem?

My personal suggestions on this problem are the followings:

  • If you don't need to dynamically change the value of the components' state(like pagination in my case), then don't be bothered to deal with it. Leave the dependency array empty no matter what your linter alerts.

  • If you have no choice but to get along with the state, make sure you provide the dependency array with JS's primitive data types. As we have mentioned above, useEffect compares the current data with the previous data in the dependency array with Object.is method. So even if newly fetched data has exactly the same content as the previous state, it is recognized as a new object by useEffect so the state gets updated, making another re-rendering of the component.

So in our example, the useEffect should be like

useEffect(() => {
    const fetchUserInfo = async () => {
      console.log("No bugs anymore");
      try {
        const response = await axios.get<User>(API_URL);
        setUserInfo(response.data);
      } catch(error) {
        if (error instanceof Error) {
          console.error(error.message);
        }
      }
    };

    fetchUserInfo();
  }, [userInfo.userId]) // => notice that "userInfo" has been replaced with "userInfo.userId", which is the number type in JS.
Enter fullscreen mode Exit fullscreen mode

Image description

That's it! I don't know if the contemporary trend is using useEffect directly or not, but I hope this article could be some help for someone out there. If there is something wrong in the article, please let me know.

Happy hacking:)

Top comments (0)