React Lifecycle Methods of a Functional Component

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.

mouting.gif

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.

updating.gif

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.

unmounting.gif

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.

no dependency.gif

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: Resembles componentWillUnmount lifecycle method. Runs when the component is unmounted.

That's it folks, hope you learned something new. Thanks for reading!

Happy Coding!