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>;
}
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:
- 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>
);
}
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
Top comments (0)