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.
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.
Example
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.)
Example
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.
Example
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.
Example
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.
Example
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.
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
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...