Picture of Bharathi Kannan

Bharathi Kannan

React useEffect Hook Flow


Photo ByJames Morden

It is important to understand the core concept of Hooks in React Components. This will increase our confidence with usage of hooks and help us understand what is actually happening inside our React components.

This post is to increase your understanding of flow of hooks in a react component with exclusive focus on the most confusing useEffect hook.

As always, let's start with Just Javascript

Take a look at the function below, which returns a string:

1function App(){
2 return 'Hello World';
5const text = App();
6console.log(text); // logs 'Hello World'

We are storing the value returned from App function in variable text and displaying it in the console. We know that Javascript is single threaded and can execute only one line at a time. The flow of execution is top-to-bottom.

When we execute the code, this is what would happen

  1. The Javascript engine first sees a function declaration from line 1 to 3
  2. Then goes to line number 5 where it sees a function being called.
  3. Then JS engine calls that function and assigns the value returned from that function into the text variable.
  4. In the next line the text is displayed in the console.

Now that we understand the flow of Javascript in general, let's explore the useEffect() hook in a react component and explore when it is called and in what order.

React useEffect

Let's explore useEffect in React on three Lifecycle phases of react component.

  1. Mount
  2. Update
  3. Unmount

useEffect on Mount

Take a look at the react component below

1function App(){
2 React.useEffect(() => {
3 console.log('useEffect Ran!')
4 }, []);
6 return(
7 <div>Hello, World!</div>
8 )

When you scan through this code and find the useEffect with empty [] dependencies, you would have guessed that this hook runs only on mount (exactly like componentdidmount). Yes, you are right, it runs just on the mount. so you would get this in console

useEffect Ran!

Lets see an example with a dependency in useEffect,

1function App() {
2 const [count, setCount] = React.useState(0);
4 React.useEffect(() => {
5 console.log("Count Changed");
6 }, [count]);
8 return (
9 <button
10 onClick={() => {
11 setCount((c) => c + 1);
12 }}
13 >
14 {count}
15 </button>
16 );

This is the classic counter example, when we scan the react component and find the useEffect with [count] dependency we would think this would run when the count changes.

So, on the first render the count is 0 and not changed, when you click the button, the count would change thus calling the useEffect hook right? lets check it out!

This would be logged on the first Mount:

Count Changed

Whaat? We didn't even click the button but the useEffect ran! Why?

Hooks are side-effects, and would be used mostly for performing any side-effects in the component, and the common side effect would be data fetching.

When compared to class Lifecycle methods, mentioning any dependency in a hook would make that hook similar to componentdidupdate. If you have componentdidupdate (in class component of course!) it would still be called on the mount phase!

This is how the hooks are designed to work. No matter how many dependencies you specify and how many hooks you create, every hook would be called on every render of the component.

If you are curious to know why the hooks are designed in this way, take a look at Fun with React Hooks in which Ryan Florence live codes useEffect hook and explains why hooks should only be called in the top level of react component

After the mount phase is completed, our useEffect in the above counter example would be called whenever the count changes.

1React.useEffect(() => {
2 console.log("Count Changed");
3}, [count]);

So the takeaway from this section is

Every hook in a component would be called on the mount phase (with or without the dependencies specified).

useEffect on Unmount

Now let's look at another example below with the Unmount behaviour.

1function Child() {
2 React.useEffect(() => {
3 console.log("Child useEffect Ran!");
5 return () => {
6 console.log("cleanUp of Child useEffect Ran!");
7 };
8 }, []);
10 return <div>Hello, From Child!</div>;
13export default function App() {
14 const [showChild, setShowChild] = React.useState(false);
16 React.useEffect(() => {
17 console.log("useEffect Ran!");
19 return () => {
20 console.log("cleanUp of useEffect Ran!");
21 };
22 }, []);
24 return (
25 <div>
26 <div>Hello, World!</div>
27 {showChild ? <Child /> : null}
28 <button
29 onClick={() => {
30 setShowChild((b) => !b);
31 }}
32 >
33 {showChild ? "Show" : "Hide"} Child
34 </button>
35 </div>
36 );

Our Parent App component renders a Child component which has useEffect with a cleanup function. This cleanup would be executed when the child component unmounts. So, When you render the component and toggle on the Hide/Show child button, You would get the corresponding logs as expected.

Clean up function in useEffect

If you have 3 useEffects in same component and all does return a cleanup function, then, when the component is unmounted, all the cleanup functions would be called.

Lets see that in action below

1function Child() {
2 React.useEffect(() => {
3 console.log("No Dependency!");
5 return () => {
6 console.log("cleanUp of No Dependency Ran!");
7 };
8 });
10 React.useEffect(() => {
11 console.log("Empty Dependency!");
13 return () => {
14 console.log("cleanUp of Empty Dependency Ran!");
15 };
16 }, []);
18 return <div>Hello, From Child!</div>;

and the output is

Multiple Return

The takeaway is

When the component is unmounted, cleanup function from all the useEffects are executed.

In comparison to class components, where we only have one componentWillUnmount this is the only part that would be executed on the unmount phase of that component.

useEffect on Update

Here comes the interesting part, when you have specified a dependency and if the effect re-runs because of any change in the specified dependencies, it would execute the cleanup functions before executing the hook.

Let's see this behaviour with an example. Open up the console section, and play around with the buttons.

Play around with example in codesandbox

You can play around with the useEffect flow sandbox to see when each effect is getting called and its order.

On the first mount, we see both the useEffects of App running, and when you click on the Increment count button, before running the no deps hook, the cleanup function is executed.

1โ–ถ๏ธ App Render Start
2๐Ÿ›‘ App Render End
3 App: useEffect no deps Cleanup ๐Ÿงน
4๐ŸŒ€ App: useEffect no deps

Similarly, when you click on Show Child button, before running the no deps hook of App, the cleanup is executed.

1โ–ถ๏ธ App Render Start
2๐Ÿ›‘ App Render End
3 โ–ถ๏ธ Child Render Start
4 ๐Ÿ›‘ Child Render End
5 App: useEffect no deps Cleanup ๐Ÿงน
6 ๐ŸŒ€ CHILD: useEffect empty []
7 ๐ŸŒ€ CHILD: useEffect no deps
8๐ŸŒ€ App: useEffect no deps

As seen above, from React v17, the cleanup of parent's effects are executed even before executing Child's useEffects.

Below GIF is the full rundown from the sandbox. We can see the cleanup functions are executed before the hook on the update/re-render phase. I have highlighted the cleanups with bigger fonts to get it easily.

Flow of Hooks

The key takeaway is,

React, when re-running an useEffect, it executes the clean up function before executing the hook.

The full picture of the flow of hooks can be understood from this flow-chart by donavon

Flow of Hooks by donavon

To Summarise

  1. Every hook in a component would be called on the mount phase (with or without the dependencies specified).
  2. When the component is unmounted, cleanup function from all the useEffects are executed.
  3. React, when re-running an useEffect, it executes the clean up function before executing the hook.

Big thanks to:

Let's Discuss On TwitterTwitter
GitHubEdit this Post On GitHub
Open Twitter Profile of Bharathi KannanOpen LinkedIn Profile of Bharathi Kannan
โค๏ธOpen sourced on Githubโค๏ธ