DEV Community

Cover image for React useEffect Explained: Understanding Mounting, Updating, and Unmounting
Saravanan Lakshmanan
Saravanan Lakshmanan

Posted on

React useEffect Explained: Understanding Mounting, Updating, and Unmounting

When learning React, two important concepts are the component lifecycle and the useEffect Hook.

A React component is not just created once and forgotten. It goes through different stages during its existence:

  1. Mounting
  2. Updating
  3. Unmounting

The useEffect Hook helps us perform side effects during these stages. Examples of side effects include fetching data from an API, working with timers, adding event listeners, and interacting with external systems.


What is a React Component?

A React component is an independent and reusable part of a user interface. In modern React, components are commonly created using JavaScript functions.

For example:

function Welcome() {
    return <h1>Welcome to React</h1>;
}
Enter fullscreen mode Exit fullscreen mode

When React uses this component, it renders the JSX returned by the function.

A component can have its own state, receive props, and respond to changes during its lifetime.

This is where the concept of the component lifecycle becomes important.


The Three Stages of a React Component

Every React component goes through three major stages:

Mounting → Updating → Unmounting
Enter fullscreen mode Exit fullscreen mode

We can understand these stages using a simple real-life example.

Think of a component like a person:

Birth → Life and Changes → Departure
Enter fullscreen mode Exit fullscreen mode

In React:

Component Created → Component Updated → Component Removed
Enter fullscreen mode Exit fullscreen mode

1. Mounting

Mounting is the first stage of a component's lifecycle.

It happens when a component is created and added to the page for the first time.

For example:

function App() {
    return <h1>Hello React</h1>;
}
Enter fullscreen mode Exit fullscreen mode

When React displays this component for the first time, the component is mounted.

The basic process is:

Component is created
        ↓
React renders the component
        ↓
The UI appears on the page
Enter fullscreen mode Exit fullscreen mode

For function components, useEffect with an empty dependency array is commonly used when an effect should run after the initial render.

Example:

import { useEffect } from "react";

function App() {

    useEffect(() => {
        console.log("Component mounted");
    }, []);

    return <h1>Hello React</h1>;
}

export default App;
Enter fullscreen mode Exit fullscreen mode

The empty dependency array is:

[]
Enter fullscreen mode Exit fullscreen mode

This tells React that the effect should run after the initial render and should not run again because of later state or prop changes.

A common use case is fetching initial data:

useEffect(() => {
    fetchData();
}, []);
Enter fullscreen mode Exit fullscreen mode

The empty array is useful when the operation should happen only when the component initially appears.


2. Updating

After a component has been mounted, it may change.

A component can update when:

  • Its state changes
  • Its props change
  • Its parent component causes it to render again

For example:

import { useState } from "react";

function Counter() {

    const [count, setCount] = useState(0);

    return (
        <div>
            <h2>{count}</h2>

            <button onClick={() => setCount(count + 1)}>
                Increase
            </button>
        </div>
    );
}

export default Counter;
Enter fullscreen mode Exit fullscreen mode

Initially:

count = 0
Enter fullscreen mode Exit fullscreen mode

When the button is clicked:

count: 0 → 1
Enter fullscreen mode Exit fullscreen mode

Because the state changed, React renders the component again with the updated value.

The process is:

State or props change
        ↓
React renders the component again
        ↓
The UI is updated
Enter fullscreen mode Exit fullscreen mode

This is called the updating stage.


Understanding useEffect

The useEffect Hook is used to perform side effects in a component.

The basic syntax is:

useEffect(() => {
    // Side effect code
}, [dependencies]);
Enter fullscreen mode Exit fullscreen mode

The second argument is called the dependency array.

The dependency array controls when the effect should run.

There are three important patterns.


Pattern 1: useEffect Without a Dependency Array

Example:

useEffect(() => {
    console.log("Effect executed");
});
Enter fullscreen mode Exit fullscreen mode

When no dependency array is provided, the effect runs after every render.

Example:

function App() {

    const [count, setCount] = useState(0);

    useEffect(() => {
        console.log("Effect executed");
    });

    return (
        <button onClick={() => setCount(count + 1)}>
            {count}
        </button>
    );
}
Enter fullscreen mode Exit fullscreen mode

The sequence is:

Initial render
      ↓
useEffect runs

Click button
      ↓
State changes
      ↓
Component renders again
      ↓
useEffect runs again
Enter fullscreen mode Exit fullscreen mode

So:

Every render → useEffect runs
Enter fullscreen mode Exit fullscreen mode

This can sometimes cause unwanted repeated operations.

For example, if an effect changes state and the effect runs after every render, it can potentially create repeated renders.

Therefore, the dependency array is important for controlling when an effect should execute.


Pattern 2: useEffect With an Empty Dependency Array

Example:

useEffect(() => {
    console.log("Effect executed");
}, []);
Enter fullscreen mode Exit fullscreen mode

The empty dependency array means that the effect does not depend on any state or prop value.

The effect runs after the initial render.

The sequence is:

Component mounts
      ↓
Initial render
      ↓
useEffect runs
      ↓
Later re-renders occur
      ↓
Effect does not run again because of those changes
Enter fullscreen mode Exit fullscreen mode

Example:

function App() {

    const [count, setCount] = useState(0);

    useEffect(() => {
        console.log("This runs after the initial render");
    }, []);

    return (
        <button onClick={() => setCount(count + 1)}>
            {count}
        </button>
    );
}
Enter fullscreen mode Exit fullscreen mode

When the button is clicked:

count changes
      ↓
Component re-renders
      ↓
useEffect does not run again
Enter fullscreen mode Exit fullscreen mode

A common example is fetching initial data:

useEffect(() => {
    fetch("https://example.com/api/data");
}, []);
Enter fullscreen mode Exit fullscreen mode

The effect is intended to run when the component initially appears.


Pattern 3: useEffect With Dependencies

Example:

useEffect(() => {
    console.log("Count changed");
}, [count]);
Enter fullscreen mode Exit fullscreen mode

Here, count is the dependency.

The effect runs:

  1. After the initial render
  2. Whenever count changes

Example:

function Counter() {

    const [count, setCount] = useState(0);
    const [count2, setCount2] = useState(10);

    useEffect(() => {
        console.log("Count changed");
    }, [count]);

    return (
        <div>

            <button onClick={() => setCount(count + 1)}>
                +
            </button>

            <button onClick={() => setCount2(count2 + 1)}>
                +2
            </button>

            <h2>Counter: {count}</h2>
            <h2>Counter 2: {count2}</h2>

        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

Now consider what happens when each button is clicked.

Clicking the first button

count changes
      ↓
Component re-renders
      ↓
[count] has changed
      ↓
useEffect runs
Enter fullscreen mode Exit fullscreen mode

Clicking the second button

count2 changes
      ↓
Component re-renders
      ↓
[count] has not changed
      ↓
useEffect does not run
Enter fullscreen mode Exit fullscreen mode

This is a very important point:

The entire component can re-render even though a particular useEffect does not run.

The dependency array controls the execution of the effect, not whether the entire component renders.


A Simple Example to Understand the Difference

Consider:

const [count, setCount] = useState(0);
const [count2, setCount2] = useState(10);

useEffect(() => {
    console.log("Effect executed");
}, [count]);
Enter fullscreen mode Exit fullscreen mode

When count changes:

count: 0 → 1

Component re-renders
        ↓
React checks [count]
        ↓
count changed
        ↓
useEffect runs
Enter fullscreen mode Exit fullscreen mode

When count2 changes:

count2: 10 → 11

Component re-renders
        ↓
React checks [count]
        ↓
count did not change
        ↓
useEffect does not run
Enter fullscreen mode Exit fullscreen mode

The component re-render and the effect execution are two different things.

This distinction is essential for understanding React.


3. Unmounting

Unmounting is the final stage of a component's lifecycle.

It happens when a component is removed from the page.

For example:

function App() {

    const [show, setShow] = useState(true);

    return (
        <div>

            {show && <Child />}

            <button onClick={() => setShow(false)}>
                Remove Component
            </button>

        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

Initially:

show = true
      ↓
Child component is displayed
Enter fullscreen mode Exit fullscreen mode

After clicking the button:

show = false
      ↓
Child component is removed
      ↓
Child component is unmounted
Enter fullscreen mode Exit fullscreen mode

The lifecycle becomes:

Mount
  ↓
Update
  ↓
Unmount
Enter fullscreen mode Exit fullscreen mode

useEffect Cleanup Function

Some effects create resources that should be stopped or removed when they are no longer needed.

Examples include:

  • Timers
  • Intervals
  • Event listeners
  • Subscriptions

The cleanup function is created by returning a function from useEffect.

Example:

useEffect(() => {

    const timer = setTimeout(() => {
        console.log("Timer completed");
    }, 1000);

    return () => {
        clearTimeout(timer);
    };

}, []);
Enter fullscreen mode Exit fullscreen mode

The cleanup function is:

return () => {
    clearTimeout(timer);
};
Enter fullscreen mode Exit fullscreen mode

When the component is removed, the cleanup code runs.

The process is:

Component mounts
      ↓
Timer starts
      ↓
Component is removed
      ↓
Cleanup function runs
      ↓
Timer is cleared
Enter fullscreen mode Exit fullscreen mode

This prevents unnecessary operations from continuing after the component is gone.


Cleanup During Updates

Cleanup is not only used when a component is unmounted.

Suppose an effect depends on a value:

useEffect(() => {

    console.log("Effect started");

    return () => {
        console.log("Cleanup executed");
    };

}, [count]);
Enter fullscreen mode Exit fullscreen mode

When count changes, React performs the following general sequence:

Previous effect cleanup
        ↓
Component updates
        ↓
New effect runs
Enter fullscreen mode Exit fullscreen mode

So the lifecycle of an effect can look like:

Effect starts
      ↓
Dependency changes
      ↓
Previous effect cleanup
      ↓
New effect starts
Enter fullscreen mode Exit fullscreen mode

This is particularly useful for subscriptions, timers, and event listeners.


Complete Component Lifecycle

The complete lifecycle can be represented like this:

                 MOUNTING
                    ↓
          Component is created
                    ↓
             Initial render
                    ↓
              useEffect runs
                    ↓
                 UPDATING
                    ↓
       State or props change
                    ↓
             Component renders
                    ↓
     Dependency value changed?
             ↙              ↘
           Yes               No
            ↓                 ↓
    Effect runs again     Effect does not run
                    ↓
                UNMOUNTING
                    ↓
       Component removed
                    ↓
            Cleanup runs
Enter fullscreen mode Exit fullscreen mode

Top comments (0)