Dev.to WebDev 🛠 Dev 👁 0 📖 6 min read

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: Mounting

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>;
}

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

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

Think of a component like a person:

Birth → Life and Changes → Departure

In React:

Component Created → Component Updated → Component Removed

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>;
}

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

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;

The empty dependency array is:

[]

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();
}, []);

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;

Initially:

count = 0

When the button is clicked:

count: 0 → 1

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

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]);

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");
});

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>
    );
}

The sequence is:

Initial render
      ↓
useEffect runs

Click button
      ↓
State changes
      ↓
Component renders again
      ↓
useEffect runs again

So:

Every render → useEffect runs

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");
}, []);

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

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>
    );
}

When the button is clicked:

count changes
      ↓
Component re-renders
      ↓
useEffect does not run again

A common example is fetching initial data:

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

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

Pattern 3: useEffect With Dependencies

Example:

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

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>
    );
}

Now consider what happens when each button is clicked.

Clicking the first button

count changes
      ↓
Component re-renders
      ↓
[count] has changed
      ↓
useEffect runs

Clicking the second button

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

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]);

When count changes:

count: 0 → 1

Component re-renders
        ↓
React checks [count]
        ↓
count changed
        ↓
useEffect runs

When count2 changes:

count2: 10 → 11

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

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>
    );
}

Initially:

show = true
      ↓
Child component is displayed

After clicking the button:

show = false
      ↓
Child component is removed
      ↓
Child component is unmounted

The lifecycle becomes:

Mount
  ↓
Update
  ↓
Unmount

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);
    };

}, []);

The cleanup function is:

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

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

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]);

When count changes, React performs the following general sequence:

Previous effect cleanup
        ↓
Component updates
        ↓
New effect runs

So the lifecycle of an effect can look like:

Effect starts
      ↓
Dependency changes
      ↓
Previous effect cleanup
      ↓
New effect starts

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
📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.