What are Side Effects?
Side effects are operations that affect something outside the component's scope. Think of them like a chef who not only cooks (renders) but also orders ingredients (fetches data), sets timers (subscriptions), or adjusts the kitchen temperature (DOM manipulation).
Introducing useEffect
useEffect is like a component's connection to the outside world. It runs after the render is committed to the screen.
import React, { useState, useEffect } from 'react';
function MyComponent() {
const [data, setData] = useState(null);
useEffect(() => {
// This runs after every render
console.log('Component rendered or updated!');
});
return <div>{/* component content */}</div>;
}
Basic useEffect Syntax
useEffect(() => {
// Setup code runs after render
// This is where you perform side effects
return () => {
// Cleanup code runs before next effect or unmount
// This is optional
};
}, [/* dependencies */]);
The Three Patterns of useEffect
1. Effect on Every Render
function LoggerComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('Component rendered! Count is:', count);
}); // No dependency array - runs after every render
return (
<button onClick={() => setCount(count + 1)}>
Increment: {count}
</button>
);
}
2. Effect Only on Mount
function WelcomeMessage() {
useEffect(() => {
console.log('Component mounted!');
alert('Welcome to our app!');
return () => {
console.log('Component will unmount!');
};
}, []); // Empty dependency array - runs once on mount
return <h1>Welcome!</h1>;
}
3. Effect with Dependencies
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
console.log('Fetching user data for ID:', userId);
fetchUser(userId).then(userData => {
setUser(userData);
});
}, [userId]); // Runs when userId changes
if (!user) return <div>Loading...</div>;
return <div>{user.name}</div>;
}
Effect Lifecycle
Common Use Cases
1. Data Fetching
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchUsers() {
try {
const response = await fetch('https://api.example.com/users');
const data = await response.json();
setUsers(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchUsers();
}, []); // Fetch once on mount
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
2. Event Listeners
function WindowSize() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
}
// Add event listener
window.addEventListener('resize', handleResize);
// Cleanup function
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // Empty deps = setup once
return (
<div>
Window size: {windowSize.width} x {windowSize.height}
</div>
);
}
3. Timers and Intervals
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// Cleanup function
return () => clearInterval(interval);
}, []); // Empty deps = setup once
return <div>Seconds: {seconds}</div>;
}
4. Subscriptions
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
// Subscribe to chat room
const subscription = subscribeToChat(roomId, (message) => {
setMessages(prev => [...prev, message]);
});
// Cleanup function
return () => {
subscription.unsubscribe();
};
}, [roomId]); // Resubscribe when roomId changes
return (
<div>
{messages.map((msg, i) => (
<div key={i}>{msg}</div>
))}
</div>
);
}
Dependencies Deep Dive
Dependencies tell React when to re-run your effect. Think of them as "watchers" - when these values change, the effect runs again.
function SearchResults({ query, filters }) {
const [results, setResults] = useState([]);
useEffect(() => {
// This effect depends on query and filters
async function fetchResults() {
const data = await searchAPI(query, filters);
setResults(data);
}
fetchResults();
}, [query, filters]); // Re-run when query or filters change
return (
<ul>
{results.map(result => (
<li key={result.id}>{result.title}</li>
))}
</ul>
);
}
Common Dependency Pitfalls
1. Missing Dependencies
// ❌ WRONG - Missing dependency
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1); // Bug: count is stale
}, 1000);
return () => clearInterval(timer);
}, []); // Missing count dependency
return <div>{count}</div>;
}
// ✅ CORRECT - Include dependency or use functional update
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1); // Use functional update
}, 1000);
return () => clearInterval(timer);
}, []); // No dependency needed
return <div>{count}</div>;
}
2. Object Dependencies
// ❌ PROBLEM - Object created on every render
function UserSearch() {
const [results, setResults] = useState([]);
const filters = { active: true, role: 'admin' }; // New object every render
useEffect(() => {
fetchUsers(filters).then(setResults);
}, [filters]); // Effect runs on every render!
return <div>{/* results */}</div>;
}
// ✅ SOLUTION 1 - Move object outside component
const filters = { active: true, role: 'admin' };
function UserSearch() {
const [results, setResults] = useState([]);
useEffect(() => {
fetchUsers(filters).then(setResults);
}, [filters]); // Now stable
return <div>{/* results */}</div>;
}
// ✅ SOLUTION 2 - Use individual values as dependencies
function UserSearch() {
const [results, setResults] = useState([]);
const active = true;
const role = 'admin';
useEffect(() => {
fetchUsers({ active, role }).then(setResults);
}, [active, role]); // Depend on primitive values
return <div>{/* results */}</div>;
}
Cleanup Functions
Cleanup functions prevent memory leaks and unwanted behavior. They're like cleaning up your workspace after cooking.
function MouseTracker() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
function handleMouseMove(e) {
setPosition({ x: e.clientX, y: e.clientY });
}
// Setup
window.addEventListener('mousemove', handleMouseMove);
// Cleanup
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, []);
return (
<div>
Mouse position: {position.x}, {position.y}
</div>
);
}
Conditional Effects
Sometimes you need to run effects conditionally:
function DataFetcher({ shouldFetch, id }) {
const [data, setData] = useState(null);
useEffect(() => {
if (!shouldFetch) return; // Early return if condition not met
let isCancelled = false;
async function fetchData() {
try {
const result = await fetchAPI(id);
if (!isCancelled) {
setData(result);
}
} catch (error) {
if (!isCancelled) {
console.error('Failed to fetch:', error);
}
}
}
fetchData();
return () => {
isCancelled = true; // Prevent setting state after unmount
};
}, [shouldFetch, id]);
return data ? <div>{data}</div> : null;
}
Custom Hooks with useEffect
You can create reusable logic by combining useEffect with custom hooks:
// Custom hook for window size
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
function handleResize() {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
// Custom hook for data fetching
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isCancelled = false;
async function fetchData() {
try {
setLoading(true);
const response = await fetch(url);
const json = await response.json();
if (!isCancelled) {
setData(json);
}
} catch (err) {
if (!isCancelled) {
setError(err);
}
} finally {
if (!isCancelled) {
setLoading(false);
}
}
}
fetchData();
return () => {
isCancelled = true;
};
}, [url]);
return { data, loading, error };
}
// Using custom hooks
function MyComponent() {
const windowSize = useWindowSize();
const { data, loading, error } = useFetch('/api/data');
return (
<div>
<p>Window width: {windowSize.width}</p>
{loading && <p>Loading...</p>}
{error && <p>Error: {error.message}</p>}
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
</div>
);
}
Common useEffect Patterns
1. Debounced Search
function SearchInput() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
if (!query) {
setResults([]);
return;
}
const timeoutId = setTimeout(() => {
searchAPI(query).then(setResults);
}, 300); // Debounce delay
return () => clearTimeout(timeoutId);
}, [query]);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<ul>
{results.map(result => (
<li key={result.id}>{result.title}</li>
))}
</ul>
</div>
);
}
2. Previous Value Tracking
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>Current: {count}</p>
<p>Previous: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}