DEV Community

Cover image for You don't know useEffect
Trung Hieu Nguyen
Trung Hieu Nguyen

Posted on • Edited on

You don't know useEffect

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:

useEffect(someFunc, [deps_array]);
Enter fullscreen mode Exit fullscreen mode

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

useEffect(func);
Enter fullscreen mode Exit fullscreen mode

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

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

    // return..
}
Enter fullscreen mode Exit fullscreen mode

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.

useEffect(func, []);
Enter fullscreen mode Exit fullscreen mode

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

For example:

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

    // return...
}
Enter fullscreen mode Exit fullscreen mode

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

useEffect(func, [variableA, varibleB,...]);
Enter fullscreen mode Exit fullscreen mode

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

For example:

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

    // return
}
Enter fullscreen mode Exit fullscreen mode

⚠️ 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:

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 (
       ...
  );
}
Enter fullscreen mode Exit fullscreen mode

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

run only once
change a
change b
Enter fullscreen mode Exit fullscreen mode

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

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;
Enter fullscreen mode Exit fullscreen mode

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

call api first time
call api when query changes
call api when page changes
Enter fullscreen mode Exit fullscreen mode

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.

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 (
    ...
  );
};
Enter fullscreen mode Exit fullscreen mode

This is the result on first mount

call api first time
Enter fullscreen mode Exit fullscreen mode

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:

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

No, try click button "Increase a",

We will get this (not in the first render)

list changed
Use effect of someFunc's called
Enter fullscreen mode Exit fullscreen mode

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 or read for detail on the official site: useCallback and useMemo

Change our code a little bit

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;
Enter fullscreen mode Exit fullscreen mode

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

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

useEffect(() => {
    // do something
    return () => {
        // do cleanup stu
    }
}, []);
Enter fullscreen mode Exit fullscreen mode

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.

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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

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

Let's change above example a little bit:

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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

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:

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>;
};
Enter fullscreen mode Exit fullscreen mode

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

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. 😈

Top comments (23)

Collapse
 
ilikebuttons profile image
ilikebuttons

Well done! I've read a lot of articles about hooks but this is the first one that actually explains what side effects are.

Two minor corrections:
Memorization should be Memoization.
Your setInterval will log every second, not every minute.

Collapse
 
trunghieu99tt profile image
Trung Hieu Nguyen

Thank you very much. :D Fixed

Collapse
 
ponikar profile image
Ponikar

Hey there, It was amazing. I am having a little doubt.
Is it worth it to memoize the component who has useEffect hook?

Every time some dependencies is going to be changed, the callback function will trigger! So what's meaning of memoization here!

Collapse
 
trunghieu99tt profile image
Trung Hieu Nguyen • Edited

It depends on the scenario, the main purpose of using Memoization here is to prevent unnecessary re-render (i.e the List only cares about fakeList, but it's re-rendered just because of changes of other stuff). Actually in real-life project, determining whether to use Memoization or not is much more complex than that.

Collapse
 
ponikar profile image
Ponikar

That's great.

Collapse
 
haosik profile image
Nikolay Mochanov

Cool article. Thanks.
By the way, the 'useCallback+useMemo' sandbox example is a bit wrong. It includes console.log for mounted List while not having fakeList declaration wrapped into useMemo.

Collapse
 
apaatsio profile image
Antti Ahti

Nice article, thanks. There are a couple of errors in the first code snippet under section 2b, though.

  1. It's missing import for useEffect
  2. It should use object destructuring in the List component, i.e. const List = memo(({ list }) => ... (note the curly braces around list)
  3. fakeList should be an array fakeList = [...] and not a function fakeList = () => [...]
Collapse
 
tanth1993 profile image
tanth1993

great. when I start to learn useEffect at the beginning. I didn't know why all effects were called at the first time even though the dependancies didn't change. Now I know the tip from you. However, I had to arrange useEffect in order so that the effect work as I expected.

Collapse
 
srikanth597 profile image
srikanth597

It's been sometime I used react,
If my memory is correct, ur useRef variable isMounted should also be automatically part of dependencies array doesn't it?
If u don't add it, IDE doesn't complain?

Maybe someone could u get this to my head.

Collapse
 
haosik profile image
Nikolay Mochanov

If I understood your question right - no, ref (isMounted in this case) should not be a part of dependencies because it is something different (from the 'state' for example) by nature. That said, changing useRef.current value does not cause the re-render, so it's just being changed 'silently' and only help the useEffects to fire when they really should.

Collapse
 
avishkardalvi profile image
AvishkarDalvi

Great post, anyone who is confused about useEffecf must read this.

Collapse
 
rjitsu profile image
Rishav Jadon

Great post! Loved the bit about using refs. I discovered this at work and looked for a solution and found it.

Collapse
 
codeofrelevancy profile image
Code of Relevancy

Saved a lot time. I really likes isMounted solution, it works well for my app. Thank you for sharing..

Collapse
 
the_yamiteru profile image
Yamiteru

I believe useEffect shouldn't even exist. It's an anti-pattern, a bad practice. Instead we should be able to subscribe to changes of individual variables.