useEffect & Lifecycle
Learn how to handle side effects like data fetching, timers, and subscriptions with the useEffect hook

Buzz presents
useEffect is how youre component does stuff like fetching data or running timers. and cleanup keeps things neat, sooo important!useEffect is how youre component does stuff like fetching data or running timers. and cleanup keeps things neat, sooo important!

Some of what your app does has nothing to do with drawing UI. Fetching data from an API. Setting up a timer. Updating the document title. Connecting to a WebSocket. Those are all side effects, meaning stuff that happens "on the side," outside React's normal render-and-display flow.
The useEffect hook is how you run side effects in function components.
What is a Side Effect?#
A side effect is anything that reaches outside your component. Here are some everyday examples.
- Fetching data from a server
- Setting up a timer or interval
- Manually changing the document title
- Reading from or writing to localStorage
- Adding an event listener to the window
None of those can happen during rendering, because they all poke at the outside world.
One heads up from the React docs. Effects are meant for syncing with stuff OUTSIDE React (a server, a timer, the browser). If you're just calculating something from props or state you already have, skip the effect and calculate it right in the component body.
Basic useEffect#
The simplest useEffect runs after every render.
import { useState, useEffect } from 'react';
function PageTitle() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
});
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}Every time this component renders (state updates included), the document title gets updated to show the current count.
The Dependency Array#
Most of the time you don't want an effect running on every render. The dependency array (that's the second argument) controls when the effect runs again.
Step 1. Run After Every Render#
Leave out the dependency array and the effect runs after every single render.
useEffect(() => {
console.log('I run after EVERY render');
});Step 2. Run Only on Mount (Once)#
Pass an empty array and the effect runs just once, when the component first shows up. (In development with Strict Mode on, React runs setup, then cleanup, then setup again one extra time on purpose to check your cleanup works. So seeing that log twice in dev is normal and NOT a bug.)
useEffect(() => {
console.log('I run only ONCE (on mount)');
}, []);Step 3. Run When Specific Values Change#
List the values your effect depends on. Then it only re-runs when one of those values changes.
useEffect(() => {
console.log(`User ID changed to: ${userId}`);
fetchUserData(userId);
}, [userId]);Fetching Data with useEffect#
The most common job for useEffect is grabbing data when a component loads.
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let ignore = false; // Flips to true if userId changes before the fetch finishes
async function fetchUser() {
try {
setLoading(true);
setError(null);
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
if (!ignore) setUser(data);
} catch (err) {
if (!ignore) setError('Failed to load user');
} finally {
if (!ignore) setLoading(false);
}
}
fetchUser();
return () => {
ignore = true; // Ignore the old response if a newer fetch started
};
}, [userId]); // Re-fetch when userId changes
if (loading) return <p>Loading...</p>;
if (error) return <p>{error}</p>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}Cleanup Functions#
Some effects set up things that have to be torn down later, like event listeners, timers or subscriptions. For those, return a cleanup function from your effect.
function WindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
// Set up the listener
window.addEventListener('resize', handleResize);
// Cleanup: remove the listener when component unmounts
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // Empty array: set up once, clean up on unmount
return <p>Window width: {width}px</p>;
}The cleanup function runs in two situations.
- When the component is removed from the page (unmounts)
- Right before the effect re-runs (when dependencies change)
Timer Example with Cleanup#

Here's a countdown timer that cleans up after itself properly.
function Countdown({ seconds }) {
const [timeLeft, setTimeLeft] = useState(seconds);
useEffect(() => {
if (timeLeft <= 0) return; // Don't set up interval if already done
const intervalId = setInterval(() => {
setTimeLeft(prev => prev - 1);
}, 1000);
// Cleanup: clear the interval
return () => clearInterval(intervalId);
}, [timeLeft]);
return <p>{timeLeft > 0 ? `${timeLeft}s remaining` : "Time's up!"}</p>;
}Skip the cleanup and that interval keeps on ticking even after the component is gone. That's how you end up with memory leaks and weird errors that make you question your life choices.
The Component Lifecycle#
Knowing WHEN things happen helps you write better effects. Here's the lifecycle of a function component.
- Mount - Component appears on screen for the first time
- Render - Component function runs, returning JSX
- DOM update - React applies changes to the real page
- Effects run - useEffect callbacks execute after painting
- Re-render - State or props change, starting from step 2
- Cleanup - Before effect re-runs or on unmount
- Unmount - Component is removed from the page
function LifecycleDemo() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('Effect ran! Count is:', count);
return () => {
console.log('Cleaning up for count:', count);
};
}, [count]);
console.log('Rendering with count:', count);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}Common useEffect Patterns#
Updating Document Title#
useEffect(() => {
document.title = `${unreadCount} new messages`;
}, [unreadCount]);Debounced Search#
useEffect(() => {
const timer = setTimeout(() => {
searchAPI(query);
}, 500);
return () => clearTimeout(timer);
}, [query]);Local Storage Sync#
useEffect(() => {
localStorage.setItem('theme', theme);
}, [theme]);TL;DR#
- useEffect runs side effects after rendering (data fetching, timers, DOM changes)
- The dependency array controls when effects re-run, so it's every render (none), once (
[]), or on changes ([value]) - Cleanup functions prevent memory leaks by removing listeners, clearing timers and canceling subscriptions
- Never make the useEffect callback itself async... define an async function inside it instead
- Components go through mount, update and unmount phases
- Always list every value you use in the dependency array to dodge stale data bugs
What's Next?#
You can fetch data, run timers and handle the component lifecycle now. Next lesson pulls it ALL together into something practical. You're building a form in React, which combines state, events, effects and validation into one solid component...
This lesson ends with a short activity.
