The Lifecycle of React Hooks Component

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.

The Lifecycle of React Hooks Component
Related Posts
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.
notion image
This image is taken from https://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: 1. Mount 1. Update 1. 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: 1. Run lazy initializers - Lazy initializers are functions that we pass to useState and useReducer. Those functions will be run only in this mount stage. 1. Render - This is where all the useState hooks and other things are present. 1. React updates DOM - Updating of DOM is not same as the browser painting the screen. 1. Run Layout Effects - We will see layout effects in future articles. 1. Browser paints the screen 1. 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. - 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. 1. useEffect with no dependencies 1. useEffect with empty dependencies 1. 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 and Child component.
  • App component has a state which decides whether to show the Child component or not.
  • Child component has a count state.
  • Child has a button to update the count.
  • Both App and Child has three types of useEffect callbacks
    • useEffect with no dependencies
    • useEffect with empty dependencies
    • useEffect with one or more dependencies.
We will see how the flow looks like for each of the following steps: 1. App is mounted 1. Child is mounted by updating the state in App 1. Child is updated by updating the count in Child 1. 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 1. βœ… Run lazy initializers of App 1. βœ… Render of App 1. βœ… React updates DOM of App 1. ❌ Cleanup layout effects of App 1. βœ… Run Layout Effects of App 1. βœ… Browser paints screen of App 1. ❌ Cleanup Effects of App 1. βœ… Run Effects of App
When the App is mounted, we see the following console logs.
  1. App: render start
      • App rendering starts.
  1. App: useState(() => false)
      • App lazy initializer is getting executed.
  1. App: showChild = false
      • App renders.
  1. App: render end
      • App rendering finishes.
  1. App: useEffect(() => {})
      • App useEffect with no dependecies is being executed.
  1. 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 the useEffect callbacks will be called.
  1. App: useEffect(() => {}, [showChild])
      • App useEffect with showChild as dependecy is being executed.
      • This is getting called because this is the mount phase of the App component, and in mount phase all the useEffect callbacks will be called.
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
  1. βœ… Run lazy initializers of Child
  1. βœ… Render of Child
  1. βœ… React updates DOM of Child
  1. ❌ Cleanup layout effects of Child
  1. βœ… Run Layout Effects of Child
  1. βœ… Browser paints screen of Child
  1. ❌ Cleanup Effects of Child
  1. βœ… Run Effects of Child
And for App,
  1. ❌ Run lazy initializers of App
  1. βœ… Render of App
  1. βœ… React updates DOM of App
  1. βœ… Cleanup layout effects of App
  1. βœ… Run Layout Effects of App
  1. βœ… Browser paints screen of App
  1. βœ… Cleanup Effects of App
  1. βœ… Run Effects of App
We will see the following console logs.
  1. App: render start
      • App rendering starts.
      • Lazy initializer will NOT be run now. It runs only on the initial mount.
  1. App: showChild = true
      • App renders.
  1. App: render end
      • App rendering finishes.
  1. Child: render start
      • Child is mounted and Child starts getting rendered.
  1. Child: useState(() => 0)
      • Child lazy initializer is getting executed since this is the mount phase of Child.
  1. Child: count = 0
      • Child renders.
  1. Child: render end .
      • Child rendering finishes.
  1. App: useEffect(() => {}) cleanup
      • App useEffect with no dependencies cleanup.
  1. App: useEffect(() => {}, [showChild]) cleanup
      • App useEffect with showChild dependencies cleanup.
      • This cleanup happens because showChild is getting updated here.
  1. Child: useEffect(() => {})
      • Child useEffect with no dependencies is being executed.
  1. 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 the useEffect callbacks will be called.
  1. 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 the useEffect callbacks will be called.
  1. App: useEffect(() => {})
      • App useEffect with no dependencies is being executed.
  1. App: useEffect(() => {}, [showChild])
      • App useEffect with showChild dependencies is being executed.
      • This is getting called because showChild has updated.
Notes: - While rendering the App component, we have <Child /> in its markup. But you can see the Child render starts after the App render ends. - This is because <Child /> is not same as calling Child function. It’s basically calling React.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
  1. ❌ Run lazy initializers of Child
  1. βœ… Render of Child
  1. βœ… React updates DOM of Child
  1. βœ… Cleanup layout effects of Child
  1. βœ… Run Layout Effects of Child
  1. βœ… Browser paints screen of Child
  1. βœ… Cleanup Effects of Child
  1. βœ… Run Effects of Child
We will see the following console logs
  1. Child: render start
      • Child rendering starts.
  1. Child: count = 1
      • Child renders
  1. Child: render end
      • Child rendering ends.
  1. Child: useEffect(() => {}) cleanup
      • Child useEffect with no dependencies cleanup.
  1. Child: useEffect(() => {}, [count]) cleanup
      • Child useEffect with count as dependency cleanup.
      • This is getting called because count has updated.
  1. Child: useEffect(() => {})
      • Child useEffect with no dependencies is being executed.
  1. Child: useEffect(() => {}, [count])
      • Child useEffect with count as dependency is being executed.
      • This is getting called because count has updated.

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
  1. ❌ Run lazy initializers of Child
  1. ❌ Render of Child
  1. ❌ React updates DOM of Child
  1. βœ… Cleanup layout effects of Child
  1. ❌ Run Layout Effects of Child
  1. ❌ Browser paints screen of Child
  1. βœ… Cleanup Effects of Child
  1. ❌ Run Effects of Child
And for App,
  1. ❌ Run lazy initializers of App
  1. βœ… Render of App
  1. βœ… React updates DOM of App
  1. βœ… Cleanup layout effects of App
  1. βœ… Run Layout Effects of App
  1. βœ… Browser paints screen of App
  1. βœ… Cleanup Effects of App
  1. βœ… Run Effects of App
We will see the following console logs
  1. App: render start
      • App rendering starts.
  1. App: showChild = false
      • App renders
  1. App: render end
      • App rendering ends
  1. Child: useEffect(() => {}) cleanup
      • Child useEffect with no dependencies cleanup
  1. 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.
  1. 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.
  1. App: useEffect(() => {}) cleanup
      • App useEffect with no dependencies clean up
  1. App: useEffect(() => {}, [showChild]) cleanup
      • App useEffect with showChild as dependency clean up.
      • This is getting called because showChild has updated here.
  1. App: useEffect(() => {})
      • App useEffect with no dependencies is getting executed
  1. App: useEffect(() => {}, [showChild])
      • App useEffect with showChild as dependency is getting executed
      • This is getting called because showChild has updated here.
And finally, when the App component also unmounts, the cleanup of all the App useEffects will be called.

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
Bhanu Teja P

Written by

Bhanu Teja P

24yo developer and blogger. I quit my software dev job to make it as an independent maker. I write about bootsrapping Feather.