The horror of useEffect
Tags: webdev, javascript, react
Published: 2022-12-07 07:58:08

Well, I am recently making my personal portfolio to re-challenge the job market, and I had no choice but to choose [React](https://reactjs.org/) 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](https://nextjs.org/) or [Remix](https://remix.run/)). 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](https://reactjs.org/docs/hooks-reference.html#useeffect), I heard a weird hissing sound from my old friend Intel Macbook Pro. 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/drhbritm03zipxdttavk.png)

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](https://jsonplaceholder.typicode.com) for simplicity. 

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

```js
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>
  );
}
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wia6j0wj8q7p4qniqk7d.png)

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

```js
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
```

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


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mflrqhp9yjyk21p943ar.png)

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](https://reactjs.org/docs/hooks-effect.html), 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6h786nxbqxdssrkc1lqk.png)

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](https://beta.reactjs.org/apis/react/useEffect#parameters), 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

```js
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.
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nan9ufcsr57ynheoxdfp.png)

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:)