15
React useEffect Hook Flow
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
function App(){
return 'Hello World';
}
const text = App();
console.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
- The Javascript engine first sees a function declaration from line 1 to 3
- Then goes to line number 5 where it sees a function being called.
- Then JS engine calls that function and assigns the value returned from that function into the
text
variable. - 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.
Let's explore useEffect in React on three Lifecycle phases of react component.
- Mount
- Update
- Unmount
Take a look at the react component below
function App(){
React.useEffect(() => {
console.log('useEffect Ran!')
}, []);
return(
<div>Hello, World!</div>
)
}
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,
function App() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
console.log("Count Changed");
}, [count]);
return (
<button
onClick={() => {
setCount((c) => c + 1);
}}
>
{count}
</button>
);
}
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 is what would be logged on the first mount of the component.
Count Changed
Whaaaat? 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
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 the mount phase 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.
React.useEffect(() => {
console.log("Count Changed");
}, [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).
Now let's look at another example below with the Unmount behaviour.
function Child() {
React.useEffect(() => {
console.log("Child useEffect Ran!");
return () => {
console.log("cleanUp of Child useEffect Ran!");
};
}, []);
return <div>Hello, From Child!</div>;
}
export default function App() {
const [showChild, setShowChild] = React.useState(false);
React.useEffect(() => {
console.log("useEffect Ran!");
return () => {
console.log("cleanUp of useEffect Ran!");
};
}, []);
return (
<div>
<div>Hello, World!</div>
{showChild ? <Child /> : null}
<button
onClick={() => {
setShowChild((b) => !b);
}}
>
{showChild ? "Hide" : "Show"} Child
</button>
</div>
);
}
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.
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
function Child() {
React.useEffect(() => {
console.log("No Dependency!");
return () => {
console.log("cleanUp of No Dependency Ran!");
};
});
React.useEffect(() => {
console.log("Empty Dependency!");
return () => {
console.log("cleanUp of Empty Dependency Ran!");
};
}, []);
return <div>Hello, From Child!</div>;
}
and the output is
The takeaway is
When the component is unmounted, cleanup function from all the useEffects would be 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.
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.
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.
βΆοΈ App Render Start
π App Render End
App: useEffect no deps Cleanup π§Ή
π App: useEffect no deps
Similarly, when you click on Show Child button, before running the no deps hook of App, the cleanup is executed.
βΆοΈ App Render Start
π App Render End
βΆοΈ Child Render Start
π Child Render End
App: useEffect no deps Cleanup π§Ή
π CHILD: useEffect empty []
π CHILD: useEffect no deps
π App: useEffect no deps
As seen above, from React v17, the cleanup of parent's effects are executed even before executing child component's useEffect(s).
Below GIF is the full rundown from the sandbox. We can see the cleanup functions are executed before the execution of hook on the update/re-render phase. I have highlighted the cleanups with bigger fonts to notice it easily.
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
I hope this post helps you with understanding of flow of useEffect
hook.
To Summarise
- Every hook in a component would be called on the Mount phase (with or without the dependencies specified).
- When the component is unmounted, cleanup function from all the useEffects are executed.
- React, when rerunning an useEffect, it executes the clean up function before executing the hook.
Big thanks to:
15