React useEffect Hook Flow
Photo By James 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';3}45const 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
- 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.
React useEffect#
Let's explore useEffect in React on three Lifecycle phases of react component.
- Mount
- Update
- Unmount
useEffect on Mount#
Take a look at the react component below
1function App(){2 React.useEffect(() => {3 console.log('useEffect Ran!')4 }, []);56 return(7 <div>Hello, World!</div>8 )9}
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);34 React.useEffect(() => {5 console.log("Count Changed");6 }, [count]);78 return (9 <button10 onClick={() => {11 setCount((c) => c + 1);12 }}13 >14 {count}15 </button>16 );17}
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!");45 return () => {6 console.log("cleanUp of Child useEffect Ran!");7 };8 }, []);910 return <div>Hello, From Child!</div>;11}1213export default function App() {14 const [showChild, setShowChild] = React.useState(false);1516 React.useEffect(() => {17 console.log("useEffect Ran!");1819 return () => {20 console.log("cleanUp of useEffect Ran!");21 };22 }, []);2324 return (25 <div>26 <div>Hello, World!</div>27 {showChild ? <Child /> : null}28 <button29 onClick={() => {30 setShowChild((b) => !b);31 }}32 >33 {showChild ? "Show" : "Hide"} Child34 </button>35 </div>36 );37}
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
1function Child() {2 React.useEffect(() => {3 console.log("No Dependency!");45 return () => {6 console.log("cleanUp of No Dependency Ran!");7 };8 });910 React.useEffect(() => {11 console.log("Empty Dependency!");1213 return () => {14 console.log("cleanUp of Empty Dependency Ran!");15 };16 }, []);1718 return <div>Hello, From Child!</div>;19}
and the output is
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 Start2๐ App Render End3 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 Start2๐ App Render End3 โถ๏ธ Child Render Start4 ๐ Child Render End5 App: useEffect no deps Cleanup ๐งน6 ๐ CHILD: useEffect empty []7 ๐ CHILD: useEffect no deps8๐ 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.
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
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 re-running an useEffect, it executes the clean up function before executing the hook.
Big thanks to:
Published:April 25, 2021
Updated:May 16, 2023