React useEffect Hook
Overview
The useEffect hook is a fundamental React hook that enables functional components to perform side effects. It replaces lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount from class components.
This guide covers:
- What
useEffectis and what side effects are - When and how to use
useEffect - Syntax and the dependency array
- Common mistakes and how to avoid them
- Additional resources for deeper learning
What is the useEffect Hook?
The useEffect hook lets you synchronize a component with external systems or perform actions that aren’t directly tied to rendering. In other words, it lets your component interact with the outside world and perform operations beyond just calculating and returning JSX.
What is a Side Effect?
A side effect is any operation that reaches outside the current function and interacts with the external world. Side effects include:
- Fetching data from an API (like getting user data from a server)
- Subscribing to events (like listening for clicks or window resizing)
- Manually updating the document title (like showing “New message (3)” in the browser tab)
- Manipulating the DOM directly (like focusing an input field)
- Setting up timers (like creating countdown timers with
setTimeoutorsetInterval)
Side effects should be kept separate from rendering logic because they can:
- Cause unpredictable behavior if executed during rendering
- Lead to performance issues if not properly managed
- Create memory leaks if not cleaned up
In simple terms: A side effect is anything your component does besides returning JSX. If your component needs to “talk” to the outside world or do something after rendering, that’s a side effect.

📖 Read more: Synchronizing with Effects
When Should I Use useEffect?
Use useEffect when your component needs to handle side effects at specific moments in its lifecycle.
1. After Component Mount
Run code once after the component is added to the DOM (appears on the screen for the first time):
useEffect(() => {
// This code runs after the component appears on screen
console.log('Component is now visible on the page');
document.title = 'My App - Home Page';
// Fetch initial data
fetch('https://api.example.com/data')
.then(res => res.json())
.then(data => console.log(data));
}, []); // Empty array = run once on mount
Code walkthrough:
- The component renders and appears on screen
- React runs the function inside
useEffect - The console logs “Component is now visible on the page”
- The browser tab title changes to “My App - Home Page”
- The fetch request is sent to get data from the API
- The empty array
[]as the second argument tells React to only run this effect once when the component first appears
Real-world example: You’d use this pattern when you need to fetch a user’s profile data as soon as their profile page loads.
2. When Dependencies Change
Run code whenever specific values (dependencies) change:
useEffect(() => {
// This runs whenever userId changes
console.log('userId changed to:', userId);
// Update document title with user name
document.title = `User: ${userId}`;
// Fetch user data based on userId
fetch(`https://api.example.com/users/${userId}`)
.then(res => res.json())
.then(data => setUserData(data));
}, [userId]); // Dependency array with userId - effect runs when userId changes
Code walkthrough:
- The component renders with a
userIdvalue - React runs the function inside
useEffect - When
userIdchanges (e.g., from “123” to “456”), React runs the effect again - The browser tab title updates to show the new userId
- A new fetch request is made to get data for the new userId
- The array
[userId]is the “dependency array” - it tells React what values to watch for changes
Real-world example: This pattern is perfect for a social media profile page where you need to load different user data when the URL or selected profile changes.
3. Cleanup
Run cleanup code before the component unmounts (is removed from the screen) or before the effect runs again:
useEffect(() => {
// Setup phase - runs after component mounts
console.log('Setting up subscription');
const timer = setInterval(() => {
console.log('Checking for new messages...');
}, 10000); // Check every 10 seconds
// Cleanup phase - runs before unmount or before next effect run
return () => {
console.log('Cleaning up subscription');
clearInterval(timer); // Stop the interval
};
}, []); // Empty array = cleanup runs on unmount
Code walkthrough:
- The component renders and appears on screen
- React runs the function inside
useEffect - A timer is set up that runs every 10 seconds
- The function returns another function (the “cleanup function”)
- When the component is about to be removed from the page, React calls this cleanup function
- The cleanup function clears the timer, preventing it from running after the component is gone
Real-world example: This pattern is essential for chat applications where you need to set up a message subscription when the chat opens and clean it up when the chat closes.

🎬 Watch: React useEffect Hook Explained
useEffect Syntax
useEffect(() => {
// Side effect code here (runs after render)
console.log('Effect ran');
// Optional cleanup function (runs before next effect or unmount)
return () => {
console.log('Cleanup ran');
};
}, [dependency1, dependency2]); // Dependency array controls when effect runs
The useEffect hook takes two arguments:
- Effect Function: The code to run for the side effect
- Dependency Array (optional): Controls when the effect runs
Dependency Array Explained
The dependency array is a list of values that your effect depends on. Think of it as telling React, “Only run this effect when these specific values change.”
There are three ways to use the dependency array:
// 1. No dependency array: Effect runs after EVERY render
useEffect(() => {
console.log('This runs after every render');
});
// 2. Empty array []: Effect runs ONLY ONCE after the first render (mount)
useEffect(() => {
console.log('This runs only once when the component appears');
return () => {
console.log('This cleanup runs only once when the component disappears');
};
}, []);
// 3. Array with values [a, b]: Effect runs on mount AND when any value in the array changes
useEffect(() => {
console.log('This runs when count changes:', count);
}, [count]);
Walkthrough of dependency array behavior:
Let’s look at an example with a counter component:
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('Guest');
// Effect 1: Runs on every render
useEffect(() => {
console.log('Effect 1: Component rendered');
});
// Effect 2: Runs only on mount
useEffect(() => {
console.log('Effect 2: Component mounted');
}, []);
// Effect 3: Runs when count changes
useEffect(() => {
console.log('Effect 3: Count changed to', count);
}, [count]);
return (
<div>
<p>Hello, {name}. You clicked {count} times.</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setName('User')}>Change Name</button>
</div>
);
}
What happens when this component runs:
-
When the component first appears (mounts):
- All three effects run
- Console shows: “Effect 1”, “Effect 2”, and “Effect 3”
-
When you click “Increment” (changing count):
- Effects 1 and 3 run (because count changed)
- Console shows: “Effect 1” and “Effect 3”
-
When you click “Change Name” (changing name):
- Only Effect 1 runs (because only name changed, not count)
- Console shows: “Effect 1”
In simple terms: The dependency array is like a list of “triggers” for your effect. When any of those triggers change, the effect runs again.
📖 Read more: useEffect Reference
What is a Dependency Array?
The dependency array is an optional second argument to useEffect that lists variables the effect depends on. It controls when the effect (and its cleanup) runs.
How Does it Relate to useEffect?
- React compares the current values of dependencies with their previous values
- If any value changes (using shallow comparison), the effect runs after the render
- If the array is empty, the effect only runs once on mount
- If there’s no array provided, the effect runs after every render

Example with Explanation
function UserProfile({ userId, theme }) {
const [user, setUser] = useState(null);
// This effect depends on userId
useEffect(() => {
console.log('Fetching user data for ID:', userId);
fetch(`https://api.example.com/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]); // Only re-run when userId changes, not when theme changes
return (
<div className={theme}>
{user ? <h1>{user.name}</h1> : <p>Loading...</p>}
</div>
);
}
In this example:
- The effect function fetches user data from an API
- The dependency array
[userId]tells React to run this effect:- When the component first mounts
- Whenever
userIdchanges - But NOT when
themechanges
- This is efficient because we only fetch user data when necessary
Real-world analogy: Think of the dependency array like a notification system. You’re telling React: “Notify this effect when any of these values change, otherwise leave it alone.”
📖 Read more: How the Dependency Array Works
Common Mistakes with useEffect
1. Infinite Loops
Forgetting to include dependencies or updating a dependency inside the effect can cause infinite re-renders.
// ❌ BAD: Creates an infinite loop!
function BadComponent() {
const [data, setData] = useState(null);
useEffect(() => {
// This fetches data and updates state...
fetch('https://api.example.com/data')
.then(res => res.json())
.then(result => setData(result));
// ...which triggers a re-render...
// ...which runs this effect again...
// ...creating an infinite loop!
}); // Missing dependency array
}
// ✅ GOOD: Empty dependency array prevents infinite loops
function GoodComponent() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('https://api.example.com/data')
.then(res => res.json())
.then(result => setData(result));
}, []); // Empty array means "run only once on mount"
}
Why this happens: Without a dependency array, the effect runs after every render. If the effect updates state with setData, it triggers another render, which runs the effect again, creating an infinite loop.
How to fix it: Add a dependency array ([] for run-once effects, or include specific dependencies if the effect should re-run when certain values change).

Fix: Use linting tools like ESLint with the react-hooks/exhaustive-deps rule to catch missing dependencies.
📖 Read more: Avoiding Infinite Loops
2. Missing Cleanup
Not cleaning up subscriptions or intervals can lead to memory leaks.
// ❌ BAD: No cleanup for timer
function BadTimer() {
useEffect(() => {
// This interval keeps running even after the component is gone
const id = setInterval(() => console.log('Tick'), 1000);
}, []);
return <p>Timer component</p>;
}
// ✅ GOOD: Proper cleanup
function GoodTimer() {
useEffect(() => {
// Set up interval
const id = setInterval(() => console.log('Tick'), 1000);
// Clean up interval when component unmounts
return () => {
console.log('Cleaning up timer');
clearInterval(id);
};
}, []);
return <p>Timer component</p>;
}
Code walkthrough:
-
Bad example:
- Sets up an interval that runs every second
- When the component unmounts (is removed from the page), the interval keeps running
- This wastes memory and can cause bugs if the timer tries to update state in a component that no longer exists
-
Good example:
- Sets up the same interval
- Provides a cleanup function that clears the interval
- When the component unmounts, React calls the cleanup function
- The interval stops running, preventing memory leaks
Real-world example: This pattern is crucial for any feature that needs to run continuously, like a live-updating clock, animation, or data polling system.
3. Overusing useEffect
Avoid using useEffect for logic that could be handled in event handlers or render logic.
// ❌ Overcomplicated: Using effect for a simple initialization
function BadRandomComponent() {
const [value, setValue] = useState(0);
// Unnecessary use of useEffect just to set an initial random value
useEffect(() => {
setValue(Math.random());
}, []);
return <div>Random value: {value}</div>;
}
// ✅ Simpler: Use initial state directly
function GoodRandomComponent() {
// Initialize state directly with the random value
const [value] = useState(Math.random());
return <div>Random value: {value}</div>;
}
Code walkthrough:
-
Bad example:
- Creates state with initial value 0
- Uses an effect to set a random number after mounting
- This causes an extra render: first with 0, then with the random number
-
Good example:
- Initializes state directly with the random number
- Only renders once with the random value
- Cleaner code with the same result
When to avoid useEffect:
- For one-time initialization: Use the initial state value
- For responding to events: Use event handlers
- For calculations from props/state: Do them directly in the render function
📖 Read more: You Might Not Need an Effect
Practical Examples
Example 1: Document Title Updater
This component updates the browser tab title based on a count:
function DocumentTitleUpdater() {
const [count, setCount] = useState(0);
// Update document title whenever count changes
useEffect(() => {
document.title = `You clicked ${count} times`;
console.log('Title updated to:', document.title);
}, [count]); // Re-run when count changes
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
Code walkthrough:
- The component displays a counter and a button
- Every time you click the button,
countincreases by 1 - The
useEffectsees thatcountchanged and runs again - It updates the browser tab title to show the new count
- The dependency array
[count]ensures the effect runs whenevercountchanges
Real-world use case: This pattern is useful for notifications, where you might update the tab title to show “(3) New Messages” when unread messages arrive.
Example 2: Data Fetching With Cleanup
This component fetches data when it mounts and cleans up properly:
function UserData({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Flag to track if component is mounted
let isMounted = true;
// Show loading state
setLoading(true);
// Fetch user data
fetch(`https://api.example.com/users/${userId}`)
.then(res => res.json())
.then(data => {
// Only update state if component is still mounted
if (isMounted) {
setUser(data);
setLoading(false);
}
})
.catch(error => {
// Handle errors only if still mounted
if (isMounted) {
console.error('Fetch error:', error);
setLoading(false);
}
});
// Cleanup function runs when component unmounts
// or before the effect runs again
return () => {
console.log('Cleaning up fetch for userId:', userId);
isMounted = false; // Prevent state updates after unmount
};
}, [userId]); // Re-run when userId changes
if (loading) return <p>Loading user data...</p>;
return user ? <div>User: {user.name}</div> : <p>No user found</p>;
}
Code walkthrough:
- The component takes a
userIdprop and has state for the user data and loading status - When the component mounts or
userIdchanges, the effect runs - It sets
loadingto true and starts fetching data - It uses an
isMountedflag to track if the component is still mounted - When data arrives, it updates state only if still mounted
- The cleanup function sets
isMountedto false if the component unmounts - This prevents the “Can’t perform a React state update on an unmounted component” warning
Real-world use case: This pattern is essential for any component that fetches data, especially when users might navigate away before the data arrives. It’s common in search features, profile pages, and dashboards.
Additional References
- 🎥 (Video) Learn useEffect in 13 minutes
- 📝 (Blog) Understanding useEffect Dependency Array
- 🔌 (ESLint Plugin) for React Hooks - Helps catch dependency issues
- 📖 Official React Documentation