You don't know useEffect
Tags: react, webdev, javascript
Published: 2021-08-15 07:29:35

useEffect is one of the most common React Hooks which every ReactJS developer should know. But using useEffect the right way is not easy as you might think. Today, I will go through some notes which I think many newbies go wrong when using useEffect and solution to get rid of those issues. 

## 1. Quick overview

I think we should start with some basics about useEffect. We all know that useEffect is a React Hook which is used to handle side effects function (for who doesn't know what's a side effect function - it's just a function that interacts with the outside world. I.e: Log something to screen, create a file, save data to database, change DOM....). 

If you know about React lifecycle, useEffect will do the job of componentDidMount, componentDidUpdate and componentWillUnmount. Yes, 3 methods in 1 hook. Therefore, the use cases of useEffect will be use cases of those above methods: 

- Calling API
- Do something when state/props change
- Cleaning stuffs on unmount / before next render
- And much more than that....

Syntax: The syntax of useEffect is very simple: 

```jsx
useEffect(someFunc, [deps_array]);
```

The first argument will be a side effect function. 

The second argument will be an array of dependencies which determine whether that useEffect would run or not. 

## 2. Dive deep into useEffect.

### a. Forms of useEffect

First, we'll talk about 3 forms of useEffect. I don't know if it's right to call "form", but at least it makes sense to me (hope it will make sense to you guys too!)

The form of useEffect is determined by the second argument: array of dependencies. 

Firstly, the deps_arrray is optional, you aren't forced to pass the second argument. In case if only pass the first argument, we have the first form of useEffect

```jsx
useEffect(func);
```

In this case, the function passed to useEffect will run on every render of the component. It's used when you need to do something on every render of the component. But you should be careful when use this form of useEffect if you don't want to mess up with infinity render or memory leak. You should avoid using this form of useEffect as much as possible

For example

```jsx
const App = () => {
	useEffect(() => {
		console.log("This effect is called on every render");
	});

	// return..
}
```

Everytime your component is re-rendered, you will see that log. 

If you pass an empty array as second argument of useEffect, you will have the second form of it. 

```jsx
useEffect(func, []);
```

It's opposite with the first form, the function passed to useEffect will run only one time (after the first render). 

For example: 

```jsx
const App = () => {
	useEffect(() => {
		console.log("Effect has been called");
	}, []);
	
	// return...
}
```

Except for the first render, you won't see the log "Effect has been called" anymore. 

The third form of useEffect is when you pass array with some variable in the array of dependencies

```jsx
useEffect(func, [variableA, varibleB,...]);
```

This time, func will be run every time there's a change with any element in array of dependencies. 

For example: 

```jsx
const App = () => {
	const [counter, setCounter] = useState(0);
	useEffect(() => {
		// This will run every time counter changed
		console.log('counter: ', counter);
	}, [counter]);
	
	// return
}
```

⚠️ There's a thing you need to notice: Even if you pass an array of dependencies or not, and you just intend to run the function in the third form of useEffect when one of the dependencies changes,  the useEffect will always run on the first time component is mounted. 

For example: 

```jsx
const App = () => {
  const [a, setA] = useState(0);
  const [b, setB] = useState(1);

  useEffect(() => {
    console.log("run only once");
  }, []);

  useEffect(() => {
    console.log("Change a");
  }, [a]);

  useEffect(() => {
    console.log("Change b");
  }, [b]);

  return (
	   ...
  );
}
```

On the first render, you'll see three logs:

```jsx
run only once
change a
change b
```

So even though a and b aren't changed at all, those useEffect associated with those variables still run on the first render. This will be a big deal if you have multi useEffect that triggers some side effect that are heavy (i.e: API call). For example, you have to render a list with pagination and search query

```jsx
import { useEffect, useState } from "react";
import "./styles.css";

const App = () => {
  const [query, setQuery] = useState(0);
  const [page, setPage] = useState(1);

  useEffect(() => {
    console.log("call api first time");
  }, []);

  useEffect(() => {
    console.log("Call api when query changes");
  }, [query]);

  useEffect(() => {
    console.log("Call api when page changes");
  }, [page]);

  return (
   ...
  );
};

export default App;
```

For the first time your component is mounted, you will see three logs: 

```jsx
call api first time
call api when query changes
call api when page changes
```

Let's imagine if you listen to changes of many other fields and on each of useEffect for those fields, you trigger API calls (or any other side effect function), so for the first time your app is rendered, a lot of unnecessary API calls will be triggered which can affect the performance of your app and cause some bugs that you might not expect (in case you don't really need to fire all API call or side effects function of all useEffect) 

To get rid of that issue, there're some ways, but I will introduce to you guys the common way - which's my favorite one to deal with that problem. You can create a variable to check if component is mounted or not. 

```jsx
const App = () => {
  const [query, setQuery] = useState(0);
  const [page, setPage] = useState(1);
  const isMounted = useRef(false);

  useEffect(() => {
    if (isMounted.current) {
      console.log("Call api when query changes");
    }
  }, [query]);

  useEffect(() => {
    if (isMounted.current) {
      console.log("Call api when page changes");
    }
  }, [page]);

  useEffect(() => {
    console.log("call api first time");
    isMounted.current = true;
  }, []);

  return (
    ...
  );
};
```

This is the result on first mount

```jsx
call api first time
```

Also notice about the order of useEffect, I didn't put them in that order for nothing. In order to make that solution works, you must put the variable that holds value for the first render/mount (or whatever you want to call it) in last useEffect.  React goes through useEffects in order

### b. Dependencies

In the previous section, I mentioned the list of dependencies passed to useEffect, by doing that, you can "listen" to any change of each element in the dependency list. 

The problem here is that : most of the time you will work with object and function, if you pass variable with object/function type to dependency list, sometimes your program might work not as you expected. Let's consider the below example:

```jsx
import { memo, useState } from "react";
const List = memo((list) => {
  useEffect(() => {
    console.log("list changed");
  }, [list]);

  return <ul>{list?.length > 0 && list.map((e) => <li>{e}</li>)}</ul>;
});

const App = () => {
  const [a, setA] = useState(0);

  const someFunc = () => console.log("This is a random function");

  useEffect(() => {
    console.log("Use effect of someFunc's called");
  }, [someFunc]);

  const fakeList = () => ["number 1", "number 2"];

  return (
    <div className="App">
      <h1>Variable a: {a} </h1>
      <button onClick={() => setA((v) => v + 1)}>Increase a</button>
      <button onClick={someFunc}>call someFunc()</button>
      <List list={fakeList} />
    </div>
  );
};
```

No, try click button "Increase a",

We will get this (not in the first render)

```jsx
list changed
Use effect of someFunc's called
```

Each time we click "Increase a" the useEffect listen to changes of someFunc and list are triggered,  even though we didn't touch or change someFunc, fakeList (Notice that I wrapped List component with memo to prevent it from re-rendering if props - list changed). It's because when comparing objects/functions, React will compare their references. So when click the button Increate a → App component will be re-rendered (due to change of state) → someFunc and fakeList are renewed , so on each render, someFunc and fakeList will have new references, therefore, React will mark that someFunc and fakeList are changed and run useEffect associated with them. You should care about this thing to prevent unnecessary re-render and unnecessary useEffect trigger

As I mentioned before, React will compare objects/functions by their references. There are 2 common cases that you should count when working with dependencies of type object/function: 

- Case 1: Objects/functions are the same, but the references are different (the case in our example).
- Case 2: Objects have different values, but their references are the same (this case happens when you partially update the object but don't trigger a re-new action).

Each of the above 2 cases will affect our useEffect which leads to unexpected behavior. 

There're many solutions to avoid those cases, I'll introduce to you guys the approach that I usually use. 

For the first case: Memoization.

Yes, to do that, we will come up with 2 new hooks (maybe you guys heard about it before: useCallback and useMemo). 

For quick ref, you guys can see the differences of these hooks here: [The difference between useCallback and useMemo](https://codethisshit.vercel.app/reactjs-interview-questions-part-1#264ff63883964394878fa7834261d338) or read for detail on the official site: [useCallback](https://reactjs.org/docs/hooks-reference.html#usecallback) and [useMemo](https://reactjs.org/docs/hooks-reference.html#usememo)

Change our code a little bit

```jsx
import { memo, useCallback, useEffect, useMemo, useState } from "react";

const List = memo((list) => {
  useEffect(() => {
    console.log("list changed");
  }, [list]);

  return <ul>{list?.length > 0 && list.map((e) => <li>{e}</li>)}</ul>;
});

const App = () => {
  const [a, setA] = useState(0);

  const someFunc = useCallback(
    () => console.log("This is a random function"),
    []
  );

  useEffect(() => {
    console.log("Use effect of someFunc's called");
  }, [someFunc]);

  const fakeList = useMemo(() => ["number 1", "number 2"], []);

  return (
    <div className="App">
      <h1>Variable a: {a} </h1>
      <button onClick={() => setA((v) => v + 1)}>Increase a</button>
      <button onClick={someFunc}>call someFunc()</button>
      <List list={fakeList} />
    </div>
  );
};

export default App;
```

I wrapped someFunc with useCallback (actually, if you use someFunc as one of the dependencies of useEffect and don't wrap it by useCallback, in case your IDE/text editor is integrated with ESLint,  you would get a warning like this: The 'someFunc' function makes the dependencies of useEffect Hook (at line 19) change on every render. To fix this, wrap the definition of 'someFunc' in its useCallback() Hook) and also wrap our fakeList with useMemo. Because of studying purpose, we will let the list of dependencies of useCallback and useMemo are blank for now, but in real-life projects, when using these hooks, you should be careful about their dependency list. 

Now if we run our program and click the Increase a button. We won't see the log come from useEffect of someFunc and list anymore (except for the first render). 

⚠️ Every line in your code comes with a cost!. useCallback and useMemo will consume the memory of your program (since it needs to store the value somewhere) so you should be careful when using these hooks, only use them when it's really necessary. 

For the second case, I will not give example because the way to get rid of that problem is to simply listen to the attribute not the object. 

But the best practice with the dependency list of useEffect is that you should always deal with primitive type as long as you can to avoid unexpected result.

Source code for this section can be found here: [https://codesandbox.io/s/hopeful-cherry-md0db?file=/src/App.js:356-388](https://codesandbox.io/s/hopeful-cherry-md0db?file=/src/App.js:356-388)

### c. Clean up function

In overview section, I said that useEffect can do the job of componenWillUnmount life cycle. it's return function in useEffect

```jsx
useEffect(() => {
	// do something
	return () => {
		// do cleanup stu
	}
}, []);
```

The return function will execute "clean up" stuff before next time function in that useEffect is called. 

Therefore, in the above example, it's equivalent to execute some code in componentWillUnmount since the form of useEffect in the above example is #2 which only runs once after the first render of the component. 

I know it's kind of abstract. So we will go through some examples, hope you guys will get it after these examples. 

 

```jsx
const List = () => {
  useEffect(() => {
    console.log("first render list");

    return () => console.log("unmount list");
  }, []);

  return <h1>This is a list</h1>;
};

const App = () => {
  const [isListVisible, setIsListVisible] = useState(true);

  useEffect(() => {
    return () => console.log("clean up on change isListVisible");
  }, [isListVisible]);

  return (
    <div className="App">
      <button onClick={() => setIsListVisible((v) => !v)}>Toggle List</button>
      {isListVisible && <List />}
    </div>
  );
};
```

Each time you click "Toggle List" you'll see two logs: One from useEffect of form #2 from List and one is from useEffect of #3 listens for change of isListVisible.

![IsListVisible](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/py934l3n48z4skdgsbwl.png)

So why clean up is necessary. So let's consider below example: 

Let's change above example a little bit: 

```jsx
const List = () => {
  useEffect(() => {
    setInterval(() => console.log("interval from list"), 1000);
    return () => console.log("unmount list");
  }, []);

  return <h1>This is a list</h1>;
};

const App = () => {
  const [isListVisible, setIsListVisible] = useState(true);

  useEffect(() => {
    return () => console.log("clean up on change isListVisible");
  }, [isListVisible]);

  return (
    <div className="App">
      <button onClick={() => setIsListVisible((v) => !v)}>Toggle List</button>
      {isListVisible && <List />}
    </div>
  );
};
```

I added a setInterval to List, it will log every 1 sec. But the point here is: even though List is unmounted, the interval will still running. 

![setInterval image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pyexabefygm0d0ywpws6.png)

So even though the component is unmounted, some side effects we put to that component's still running. In our example, it's just an interval, but in real life, what if it's a bunch of API calls, a bunch of other side effect stuff, imagine that they still run even if their components are unmounted, it's could be a black hole that affects our app performance. 

In our example, to resolve the issue, we could simply add clearInterval to our clean up function: 

```jsx
const List = () => {
  useEffect(() => {
    const listInterval = setInterval(
      () => console.log("interval from list"),
      1000
    );
    return () => {
      console.log("unmount list");
      clearInterval(listInterval);
    };
  }, []);

  return <h1>This is a list</h1>;
};
```

I know that if you're newbie, sometimes you'll not notice about clean up stuffs, but they're really necessary and you should spend time considering about them. 

Code for this section can be found here: [https://codesandbox.io/s/flamboyant-andras-xo86e?file=/src/App.js:69-357](https://codesandbox.io/s/flamboyant-andras-xo86e?file=/src/App.js:69-357)

## 3.Summary

Okay, so I've gone through some deep information about useEffect. Besides the notes that I mentioned, there're much more cases you should notice when using useEffect to make your app work the best way it could. So keep learning and if you have any questions or corrections, please drop a comment and I will check it out. Thank you. Bye bye. 😈