In this article, we will see the order in which different useEffect
callbacks and cleanups happen. We will also see how it differs when the app mounts, unmounts, updates.
This image is taken from github.com/donavon/hook-flow.
I took the example shown in this article from
React Hooks
workshop in EpicReact.Dev by Kent C. Dodds.I have added relevant links at the end of this article. Check those out for more detailed video explanations given by Kent C. Dodds.
Every component has three phases:
- Mount
- Update
- Unmount
Mount - Hooks Flow
This stage is when the component initially mounts on a page. In this stage, the flow of hooks is as follows:
- Run lazy initializers
- Lazy initializers are functions that we pass to
useState
anduseReducer
. Those functions will be run only in this mount stage.
- Lazy initializers are functions that we pass to
- Render
- This is where all the
useState
hooks and other things are present.
- This is where all the
- React updates DOM
- Updating of DOM is not same as the browser painting the screen.
- Run Layout Effects
- We will see layout effects in future articles.
- Browser paints the screen
- Run Effects
Update - Hooks Flow
This stage is when the component updates. An update can happen for all the following reasons:
- Parent of the component re-renders
- State of the component changes
- Context changes
In this stage, the flow of hooks is as follows:
- Render
- React updates DOM
- Cleanup Layout Effects
- (Like
useEffect
)useLayoutEffect
also has a cleanup phase.
- (Like
- Run Layout Effects
- Browser paints the screen
- Cleanup Effects
- Run Effects
As you can see, this is similar to what we saw for the mount stage, except that this also has Cleanup Layout Effects
and Cleanup Effects
.
Unmount - Hooks Flow
This stage is when the component unmounts from a page.
In this stage, the flow of hooks is as follows:
- Cleanup Layout Effects
- Cleanup Effects
Only cleanups will be run in this stage.
Types of useEffect callbacks
Before we see an example, let's take a look at 3 different types of useEffect
callbacks.
- useEffect with no dependencies
- useEffect with empty dependencies
- useEffect with some dependencies
1. useEffect with no dependencies
useEffect(() => {
console.log('useEffect(() => {})') // Line 1
return () => {
console.log('useEffect(() => {}) cleanup') // Line 2
}
})
This useEffect
callback has no dependencies.
- Callback function(Line 1) will be run when:
- Component is mounted
- Component is updated
- Cleanup function(Line 2) will be run when:
- Component is updated
- Component is unmounted
2. useEffect with empty dependencies
useEffect(() => {
console.log('useEffect(() => {}, [])') // Line 1
return () => {
console.log('useEffect(() => {}, []) cleanup') // Line 2
}
}, [])
This useEffect
callback has empty dependencies.
- Callback function(Line 1) will be run when:
- Component is mounted
- Cleanup function(Line 2) will be run when:
- Component is unmounted
Note: This useEffect
callback will not be executed when the component updates because of the empty dependency array.
3. useEffect with some dependencies
useEffect(() => {
console.log('useEffect(() => {}, [count])') // Line 1
return () => {
console.log('useEffect(() => {}, [count]) cleanup') // Line 2
}
}, [count])
This useEffect
callback has one or more dependencies.
- Callback function(Line 1) will be run when:
- Component is mounted
- Any of the dependency is changed - In this case when the count is changed.
- Cleanup function(Line 2) will be run when:
- Any of the dependency is changed - In this case when the count is changed.
- Component is unmounted
Example
Consider the below example
import React from "react";
function App() {
console.log("App: render start");
const [showChild, setShowChild] = React.useState(() => {
console.log("App: useState(() => false)");
return false;
});
console.log(`App: showChild = ${showChild}`);
React.useEffect(() => {
console.log("App: useEffect(() => {})");
return () => {
console.log("App: useEffect(() => {}) cleanup");
};
});
React.useEffect(() => {
console.log("App: useEffect(() => {}, [])");
return () => {
console.log("App: useEffect(() => {}, []) cleanup");
};
}, []);
React.useEffect(() => {
console.log("App: useEffect(() => {}, [showChild])");
return () => {
console.log("App: useEffect(() => {}, [showChild]) cleanup");
};
}, [showChild]);
const element = (
<>
<label>
<input
type="checkbox"
checked={showChild}
onChange={(e) => setShowChild(e.target.checked)}
/>{" "}
show child
</label>
<div>
{showChild ? <Child /> : null}
</div>
</>
);
console.log("App: render end");
return element;
}
import React from "react";
function Child() {
console.log(" Child: render start");
const [count, setCount] = React.useState(() => {
console.log(" Child: useState(() => 0)");
return 0;
});
console.log(` Child: count = ${count}`);
React.useEffect(() => {
console.log(" Child: useEffect(() => {})");
return () => {
console.log(" Child: useEffect(() => {}) cleanup");
};
});
React.useEffect(() => {
console.log(" Child: useEffect(() => {}, [])");
return () => {
console.log(" Child: useEffect(() => {}, []) cleanup");
};
}, []);
React.useEffect(() => {
console.log(" Child: useEffect(() => {}, [count])");
return () => {
console.log(" Child: useEffect(() => {}, [count]) cleanup");
};
}, [count]);
const element = (
<button onClick={() => setCount((previousCount) => previousCount + 1)}>
{count}
</button>
);
console.log(" Child: render end");
return element;
}
- we have an
App
component andChild
component. App
component has a state which decides whether to show theChild
component or not.Child
component has acount
state.Child
has a button to update thecount
.- Both
App
andChild
has three types ofuseEffect
callbacksuseEffect
with no dependenciesuseEffect
with empty dependenciesuseEffect
with one or more dependencies.
We will see how the flow looks like for each of the following steps:
- App is mounted
- Child is mounted by updating the state in App
- Child is updated by updating the count in Child
- Child is unmounted by updating the state in App
1. App is mounted
Here the App
is in mount phase, so from the diagram, the order should be
- ✅ Run lazy initializers of App
- ✅ Render of App
- ✅ React updates DOM of App
- ❌ Cleanup layout effects of App
- ✅ Run Layout Effects of App
- ✅ Browser paints screen of App
- ❌ Cleanup Effects of App
- ✅ Run Effects of App
When the App
is mounted, we see the following console logs.
- App: render start
- App rendering starts.
- App: useState(() => false)
- App lazy initializer is getting executed.
- App: showChild = false
- App renders.
- App: render end
- App rendering finishes.
- App: useEffect(() => {})
- App
useEffect
with no dependecies is being executed.
- App
- App: useEffect(() => {}, [])
- App
useEffect
with empty dependecies is being executed. - This is getting called because this is the mount phase of the
App
component, and in mount phase all theuseEffect
callbacks will be called.
- App
- App: useEffect(() => {}, [showChild])
- App
useEffect
withshowChild
as dependecy is being executed. - This is getting called because this is the mount phase of the
App
component, and in mount phase all theuseEffect
callbacks will be called.
- App
Notes:
- All the
useEffect
callbacks will get executed on the initial mount of the component useEffect
callbacks will be run in the order in which they appear.
2. Child is mounted by updating the state in App
Let's click on show child
checkbox. This will mount the Child
component.
Here Child
will be in the mount phase and App
will be in the update phase.
As per diagram, the order for Child
will be
- ✅ Run lazy initializers of Child
- ✅ Render of Child
- ✅ React updates DOM of Child
- ❌ Cleanup layout effects of Child
- ✅ Run Layout Effects of Child
- ✅ Browser paints screen of Child
- ❌ Cleanup Effects of Child
- ✅ Run Effects of Child
And for App
,
- ❌ Run lazy initializers of App
- ✅ Render of App
- ✅ React updates DOM of App
- ✅ Cleanup layout effects of App
- ✅ Run Layout Effects of App
- ✅ Browser paints screen of App
- ✅ Cleanup Effects of App
- ✅ Run Effects of App
We will see the following console logs.
- App: render start
- App rendering starts.
- Lazy initializer will NOT be run now. It runs only on the initial mount.
- App: showChild = true
- App renders.
- App: render end
- App rendering finishes.
- Child: render start
- Child is mounted and Child starts getting rendered.
- Child: useState(() => 0)
- Child lazy initializer is getting executed since this is the mount phase of Child.
- Child: count = 0
- Child renders.
- Child: render end .
- Child rendering finishes.
- App: useEffect(() => {}) cleanup
- App useEffect with no dependencies cleanup.
- App: useEffect(() => {}, [showChild]) cleanup
- App useEffect with
showChild
dependencies cleanup. - This cleanup happens because
showChild
is getting updated here.
- App useEffect with
- Child: useEffect(() => {})
- Child useEffect with no dependencies is being executed.
- Child: useEffect(() => {}, [])
- Child useEffect with empty dependencies is being executed.
- This is getting called because this is the mount phase of the
Child
component, and in mount phase all theuseEffect
callbacks will be called.
- Child: useEffect(() => {}, [count])
- Child useEffect with
count
as dependency is being executed. - This is getting called because this is the mount phase of the
Child
component, and in mount phase all theuseEffect
callbacks will be called.
- Child useEffect with
- App: useEffect(() => {})
- App useEffect with no dependencies is being executed.
- App: useEffect(() => {}, [showChild])
- App useEffect with
showChild
dependencies is being executed. - This is getting called because
showChild
has updated.
- App useEffect with
Notes:
- While rendering the
App
component, we have<Child />
in its markup. But you can see theChild
render starts after theApp
render ends. - This is because
<Child />
is not same as calling callingChild
function. It's basically callingReact.createElement(Child)
. - React will only start calling
Child
when it's time for rendering it.
3. Child is updated by updating the count in Child
Let's click on the count
button to update the count
present in Child
.
Here Child
will be in the update phase and App
has no change.
As per diagram, the order for Child
will be
- ❌ Run lazy initializers of Child
- ✅ Render of Child
- ✅ React updates DOM of Child
- ✅ Cleanup layout effects of Child
- ✅ Run Layout Effects of Child
- ✅ Browser paints screen of Child
- ✅ Cleanup Effects of Child
- ✅ Run Effects of Child
We will see the following console logs
- Child: render start
- Child rendering starts.
- Child: count = 1
- Child renders
- Child: render end
- Child rendering ends.
- Child: useEffect(() => {}) cleanup
- Child useEffect with no dependencies cleanup.
- Child: useEffect(() => {}, [count]) cleanup
- Child useEffect with
count
as dependency cleanup. - This is getting called because
count
has updated.
- Child useEffect with
- Child: useEffect(() => {})
- Child useEffect with no dependencies is being executed.
- Child: useEffect(() => {}, [count])
- Child useEffect with
count
as dependency is being executed. - This is getting called because
count
has updated.
- Child useEffect with
4. Child is unmounted by updating the state in App
Let's click on the show child
checkbox to unmount the Child
component.
Here Child
will be in unmount phase and App
will be in update phase
As per diagram, the order for Child
will be
- ❌ Run lazy initializers of Child
- ❌ Render of Child
- ❌ React updates DOM of Child
- ✅ Cleanup layout effects of Child
- ❌ Run Layout Effects of Child
- ❌ Browser paints screen of Child
- ✅ Cleanup Effects of Child
- ❌ Run Effects of Child
And for App,
- ❌ Run lazy initializers of App
- ✅ Render of App
- ✅ React updates DOM of App
- ✅ Cleanup layout effects of App
- ✅ Run Layout Effects of App
- ✅ Browser paints screen of App
- ✅ Cleanup Effects of App
- ✅ Run Effects of App
We will see the following console logs
- App: render start
- App rendering starts.
- App: showChild = false
- App renders
- App: render end
- App rendering ends
- Child: useEffect(() => {}) cleanup
- Child useEffect with no dependencies cleanup
- Child: useEffect(() => {}, []) cleanup
- Child useEffect with empty dependencies cleanup
- This is getting called here because this in unmount phase and in unmount phase all the cleanups will be called.
- Child: useEffect(() => {}, [count]) cleanup
- Child useEffect with
count
as dependency cleanup - This is getting called here because this in unmount phase and in unmount phase all the cleanups will be called.
- Child useEffect with
- App: useEffect(() => {}) cleanup
- App useEffect with no dependencies clean up
- App: useEffect(() => {}, [showChild]) cleanup
- App useEffect with
showChild
as dependency clean up. - This is getting called because
showChild
has updated here.
- App useEffect with
- App: useEffect(() => {})
- App useEffect with no dependencies is getting executed
- App: useEffect(() => {}, [showChild])
- App useEffect with
showChild
as dependency is getting executed - This is getting called because
showChild
has updated here.
- App useEffect with
And finally, when the App
component also unmounts, the cleanup
of all the App
useEffect
s will be called.
Links and References:
- Hooks Flow in
EpicReact.Dev
by Kent C. Dodds - Understand the React Hook Flow in
The Beginners Guide To React
by Kent C. Dodds - Hook Flow Diagram
What's Next?
In the next article, we will look at what lifting state and colocating state mean in React. And also we will see when they will be useful.
Until Next Time 👋
If you liked this article, check out
- React Hooks: Managing State With useState Hook
- How to Create a Reusable LocalStorage Hook
- Easily Detect Outside Click Using useRef Hook
If you have any comments, please leave them below or you can also @ me on Twitter (@pbteja1998), or feel free to follow me.