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 Updating 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. A React component is an independent and reusable
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
- Updating
- 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:
- After the initial render
- Whenever
countchanges
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


