useEffect & Side Effects
React components should be pure during render — same input, same output, no side effects. But real apps need to fetch data, subscribe to events, and set timers. That's what useEffect is for.
The Basic Pattern
basic.jsx
import { useState, useEffect } from 'react';
function PageTitle({ title }) {
// Run after every render where title changed
useEffect(() => {
document.title = title;
console.log('Title updated to:', title);
}, [title]); // dependency array
return <h1>{title}</h1>;
}
The Dependency Array
The second argument to useEffect controls when it runs:
dependencies.jsx
import { useEffect } from 'react';
// No dependency array: runs after EVERY render
useEffect(() => {
console.log('Runs after every render');
});
// Empty array []: runs ONCE after first render (mount)
useEffect(() => {
console.log('Runs once on mount');
}, []);
// With values: runs when those values change
useEffect(() => {
console.log('userId changed to:', userId);
}, [userId]);
useEffect(() => {
console.log('query or page changed');
}, [query, page]);
Fetching Data
The most common use case — fetching data when a component mounts:
fetch.jsx
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
setError(null);
fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
.then(res => {
if (!res.ok) throw new Error('User not found');
return res.json();
})
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, [userId]); // Re-fetch when userId changes
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
if (!user) return null;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
Cleanup Functions
Effects can return a cleanup function that runs before the next effect or when the component unmounts:
cleanup.jsx
import { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// Cleanup: clears interval when component unmounts
// or before the effect runs again
return () => clearInterval(interval);
}, []); // Run once
return <p>Elapsed: {seconds}s</p>;
}
function ChatRoom({ roomId }) {
useEffect(() => {
const socket = connectToRoom(roomId);
return () => {
socket.disconnect(); // Cleanup on unmount or roomId change
};
}, [roomId]);
return <div>Chat room: {roomId}</div>;
}
Think of useEffect as "after each render, do this". The dependency array narrows "after each render" to "after renders where these values changed".