React Lifecycle Methods of a Functional Component
All about the React useEffect() hook
Hooks have become an integral part of React ever since they were introduced. React Hooks can be used in a functional component, and they let us manipulate the state and other important features without writing a class.
One such important Hook introduced in React 16.8 is the useEffect Hook. The useEffect Hook controls the entire Lifecycle of a React Component.
Every React component goes through a series of phases, which is known as the Lifecycle of that component.
The useEffect Hook
The React useEffect hook lets us perform side-effects in a functional component. Any change that does not deal with the change in the UI can be called a side-effect. These side-effects may involve making API calls, adding/removing event listeners, etc.
If you have worked with React classes, then you must be familiar with the lifecycle methods componentDidMount
, componentDidUpdate
, and componentWillUnmount
.
In a React functional component, the useEffect Hook does the job of all these 3 lifecycle methods, single-handedly. Amazing, isn't it?
But does it do that? Let's take a look at the working of the useEffect Hook and see how it resembles the lifecycle methods.
Mounting
Mounting is a term that is used to represent the phase in the lifecycle of the component when the component is rendered on the UI for the first time.
In class-based components, there was a special method for this phase. It was the
componentDidMount
lifecycle method. It runs only once when the component is initially rendered on the screen, i.e. when the component is mounted.
To depict the same phase using the useEffect hook, let's take a look at the following example.
Consider two components, App
and Counter
. Here Counter
is the child of App
.
// App.js
1. import { Counter } from "./Counter";
2. import { useState } from "react";
3. function App() {
4. const [showCounter, setShowCounter] = useState(false);
5. return (
6. <div className="App">
7. <h2>Hello World</h2>
8. <button onClick={() => setShowCounter((prev) => !prev)}>
9. {showCounter ? "Hide" : "Show"} Counter
10. </button>
11. {showCounter ? <Counter /> : null}
12. </div>
13. );
14. }
App
consists of a heading and a button that is used to show/hide or mount/unmount the Counter
component.
// Counter.jsx
1. import { useState, useEffect } from "react";
2. export function Counter() {
3. const [count, setCount] = useState(0);
4. // componentDidMount
5. useEffect(() => {
6. console.log("Component Mounted");
7. }, []);
8. return (
9. <div>
10. <h1>{count}</h1>
11. <button onClick={() => setCount((prev) => prev + 1)}>+</button>
12. <button onClick={() => setCount((prev) => prev - 1)}>-</button>
13. </div>
14. );
15. }
The Counter
component displays the value of count on the screen. It has 2 buttons, to increment and decrement the value of the counter. (We don't need them right now!)
Observe line number 5, which has the useEffect
Hook. It takes 2 parameters, the first being the function to be executed and the second being the dependency array. Here, the dependency array is empty.
A useEffect hook with an empty array passed as a dependency will run only once when the component is mounted. This way, the useEffect Hook resembles the componentDidMount
lifecycle method.
Let's see the working of the above example.
Bonus: Read this article to know why is the useEffect
hook running twice.
Notice that the useEffect
is running only when the component is initially mounted, and not when the component is updated or unmounted.
Updating
Primarily, a component can update due to 2 reasons:
- When the props it receives has updated
- When it has a state that is updated
We will see an example of the component which updates due to an update in its state.
Consider the same example, we have two components App
and Counter
.
// App.js
1. import { Counter } from "./Counter";
2. import { useState } from "react";
3. function App() {
4. const [showCounter, setShowCounter] = useState(false);
5. return (
6. <div className="App">
7. <h2>Hello World</h2>
8. <button onClick={() => setShowCounter((prev) => !prev)}>
9. {showCounter ? "Hide" : "Show"} Counter
10. </button>
11. {showCounter ? <Counter /> : null}
12. </div>
13. );
14. }
// Counter.jsx
1. import { useState, useEffect } from "react";
2. export function Counter() {
3. const [count, setCount] = useState(0);
4. // componentDidUpdate
5. useEffect(() => {
6. console.log("Component Updated");
7. }, [count]);
8. return (
9. <div>
10. <h1>{count}</h1>
11. <button onClick={() => setCount((prev) => prev + 1)}>+</button>
12. <button onClick={() => setCount((prev) => prev - 1)}>-</button>
13. </div>
14. );
15. }
Here, if you observe carefully, I made a minor change in the code compared to the previous example. I hope you can notice it!
Yes, I changed the dependency array of the useEffect
hook. It is no longer an empty array, but we have passed the state variable count
as a dependency.
This means that our useEffect
will run every time there is a change in the count
variable. This is how it depicts the componentDidUpdate
lifecycle method. You can pass multiple variables in the dependency array.
Wait, but does that mean that the useEffect
won't run on the initial render? Well, no. That's not the case. Even though it runs on every update, it will also run on the initial render as well. So, we can call this case a mixture of componentDidMount
and componentDidUpdate
.
Let's see the live demo.
Notice how the useEffect
runs on the initial render as well as every time we update the count
variable.
Unmounting
Unmounting is the term that is used to represent the phase in the component lifecycle when the component is removed from the DOM.
This is also known as the cleanup phase of the component, and all sorts of cleaning up of memory can be done in this phase.
In class-based components, this phase was represented by a special method known as componentWillUnmount
, which would run when the component is unmounted.
Consider the same example, we have two components App
and Counter
.
// App.js
1. import { Counter } from "./Counter";
2. import { useState } from "react";
3. function App() {
4. const [showCounter, setShowCounter] = useState(false);
5. return (
6. <div className="App">
7. <h2>Hello World</h2>
8. <button onClick={() => setShowCounter((prev) => !prev)}>
9. {showCounter ? "Hide" : "Show"} Counter
10. </button>
11. {showCounter ? <Counter /> : null}
12. </div>
13. );
14. }
// Counter.jsx
import { useState, useEffect } from "react";
export function Counter() {
const [count, setCount] = useState(0);
// componentWillUnmount
useEffect(() => {
return () => {
console.log("Component Unmounted");
};
}, []);
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount((prev) => prev + 1)}>+</button>
<button onClick={() => setCount((prev) => prev - 1)}>-</button>
</div>
);
}
Notice the change in the useEffect
Hook, this time we are returning a function. This return statement acts similarly to the componentWillUnmount
lifecycle method.
This means that the return statement will run when the component unmounts and can be used to perform cleanups in our application.
Let's see the live demo.
Bonus: Read this article to know why the useEffect
also ran on the initial render.
So, as discussed above, the useEffect
ran when the component was unmounted, and not on any state updates.
We got a special case to cover before we wrap up!
useEffect without a dependency array
Yes, in this case, we do not pass any dependency array to the useEffect
hook. It has only one parameter, the function to be executed.
What happens in this situation?
Well, here our useEffect
will run every time our component re-renders.
Consider the same example, we have two components App
and Counter
.
// App.js
1. import { Counter } from "./Counter";
2. import { useState } from "react";
3. function App() {
4. const [showCounter, setShowCounter] = useState(false);
5. return (
6. <div className="App">
7. <h2>Hello World</h2>
8. <button onClick={() => setShowCounter((prev) => !prev)}>
9. {showCounter ? "Hide" : "Show"} Counter
10. </button>
11. {showCounter ? <Counter /> : null}
12. </div>
13. );
14. }
// Counter.jsx
1. import { useState, useEffect } from "react";
2. export function Counter() {
3. const [count, setCount] = useState(0);
4. useEffect(() => {
5. console.log("useEffect ran");
6. });
7. return (
8. <div>
9. <h1>{count}</h1>
10. <button onClick={() => setCount((prev) => prev + 1)}>+</button>
11. <button onClick={() => setCount((prev) => prev - 1)}>-</button>
12. </div>
13. );
14. }
Notice how we have not passed any dependency to our useEffect
on line 6. This useEffect
should run after every render.
Let's see it in action.
Notice how the useEffect
ran on the initial render as well as after every subsequent re-render.
Conclusion
We learned in-depth about the useEffect
hook and how it can handle all the lifecycle methods. Let's revise it quickly:
- Empty dependency array: Resembles
componentDidMount
lifecycle method. Runs only when the component is mounted - Dependency array containing variables: Resembles
componentDidUpdate
lifecycle method. Runs whenever the variables in the dependency array update - Return inside
useEffect
: ResemblescomponentWillUnmount
lifecycle method. Runs when the component is unmounted.
That's it folks, hope you learned something new. Thanks for reading!
Happy Coding!