Hey!
Let me ask you something.
You are building a React component. When the page loads — you want to fetch some data from an API.
So you try this:
function UserProfile() {
const [user, setUser] = useState(null);
fetch("https://api.example.com/user")
.then(res => res.json())
.then(data => setUser(data));
return <div>{user ? user.name : "Loading..."}</div>;
}
Run it. Check your network tab.
The fetch is firing again. And again. And again. Infinite loop.
Every fetch updates state. Every state update re-renders the component. Every re-render calls fetch again. It never stops.
That is the problem. And useEffect is the solution.
1. Why Does This Happen?
Every time state changes — React re-renders the component. That means every line of code inside the function runs again from top to bottom.
So the fetch runs. It updates state. State triggers a re-render. Fetch runs again. Forever.
For things like fetching data or setting up a timer — you do not want them running on every render. You want them to run at specific times.
That is exactly what useEffect does.
2. What Is useEffect?
useEffect is a React hook that lets you run code at a specific time — not on every render.
Think of it like a trigger. You tell React — "when this specific thing happens, run this code."
import { useEffect } from "react";
useEffect(() => {
// code you want to run
}, [dependencies]);
Let us break this down.
() => { } — the code you want to run. This is called the effect.
[dependencies] — the dependency array. This controls when the effect runs. This is the most important part.
3. The Dependency Array — How It Controls useEffect
There are three ways to write it. Each behaves differently.
No array — runs after every render
useEffect(() => {
console.log("runs every render");
});
Rarely what you want.
Empty array — runs only once on mount
useEffect(() => {
console.log("runs once when component loads");
}, []);
Perfect for fetching data when the page loads.
Array with a value — runs when that value changes
useEffect(() => {
console.log("runs when count changes");
}, [count]);
Runs once on mount — and again every time count changes.
Quick question for you.
If you want to fetch user data only once when the page loads — which one should you use?
Empty array. The fetch runs once. Data comes back. State updates. Screen shows the result. No loop.
4. Fix the Infinite Loop
Now let us fix the broken code from the beginning.
// Broken
function UserProfile() {
const [user, setUser] = useState(null);
fetch("https://api.example.com/user")
.then(res => res.json())
.then(data => setUser(data));
return <div>{user ? user.name : "Loading..."}</div>;
}
// Fixed
function UserProfile() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch("https://api.example.com/user")
.then(res => res.json())
.then(data => setUser(data));
}, []);
return <div>{user ? user.name : "Loading..."}</div>;
}
The fetch is now inside useEffect with an empty array.
Runs once. Gets the data. Updates state. Screen shows the name. No loop.
One small change. Completely different behavior.
5. The Component Lifecycle
Every React component goes through three stages.
Mount — the component appears on screen for the first time.
Update — the component re-renders because state or props changed.
Unmount — the component is removed from the screen.
useEffect connects to all three stages.
Component mounts
→ useEffect with [] runs here
State changes
→ Component updates
→ useEffect with [value] runs here
Component unmounts
→ Cleanup function inside useEffect runs here
6. Example 1 — Fetch Data on Page Load
import { useState, useEffect } from "react";
function PostList() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/posts")
.then(res => res.json())
.then(data => {
setPosts(data);
setLoading(false);
});
}, []);
if (loading) return <p>Loading posts...</p>;
return (
<ul>
{posts.slice(0, 5).map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
What happens step by step:
Component loads
→ loading is true → "Loading posts..." shows
→ useEffect fires once
→ fetch gets the data
→ setPosts saves the data
→ setLoading sets to false
→ Component re-renders
→ Posts show on screen
Empty array. Fetch runs once. That is all you need.
7. Example 2 — Timer with Cleanup
import { useState, useEffect } from "react";
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
return <p>Time: {seconds} seconds</p>;
}
Two parts inside this useEffect.
Setup — starts a timer that increases seconds every 1 second.
Cleanup — the return () => {} part. This runs when the component is removed from the screen.
Why do you need cleanup?
If the component is removed but the timer keeps running — it will try to update state on a component that no longer exists. That causes memory leaks and errors.
Think of it like this. You turn a fan on when you enter a room. When you leave — you turn it off. You do not leave it running.
The cleanup function is you turning the fan off.
Quick question for you.
What happens if you remove the return () => clearInterval(interval) line?
The timer keeps running even after the component is gone. Every second it tries to update state on something that no longer exists. That is a memory leak.
Always clean up what you set up.
8. One Common Mistake
Missing a value in the dependency array.
// Wrong — count is used inside but missing from array
useEffect(() => {
console.log(count);
}, []);
// Right
useEffect(() => {
console.log(count);
}, [count]);
If you use a value inside useEffect — it must be in the dependency array.
If you forget it — the effect uses the old stale value and gives you wrong results.
Quick Summary — 4 Things to Remember
useEffect runs code at specific times — not on every render. The dependency array controls when.
Empty array
[]— runs once when the component mounts. Use this for fetching data on load.Array with a value
[value]— runs on mount and every time that value changes.Cleanup function — return a function from useEffect to stop timers or subscriptions when the component unmounts. Always clean up what you set up.
useEffect feels confusing at first because it is about timing — when does this code run and why.
But once that clicks — you will see it used everywhere. Data fetching. Timers. DOM updates. All of it lives inside useEffect.
Try the examples above. Change the dependency array. Add a console.log inside. Watch when it fires. That hands-on time builds real understanding.
If something did not click — drop a comment below. Happy to explain it differently.
Thanks for reading. Keep building.
Top comments (0)