Explain the useEffect hook and its common use cases.
The useEffect
hook is one of React’s built-in hooks that allows you to perform side effects in functional components. Side effects are operations that affect something outside the scope of the current function, such as data fetching, subscriptions, manual DOM manipulations, logging, and more.
Basic Syntax and Behavior
The useEffect
hook takes two arguments: a function that contains the side effect code, and an optional dependency array.
useEffect(() => {
// Side effect code here
// Optional cleanup function
return () => {
// Cleanup code here
};
}, [dependencies]); // Optional dependency array
- The effect function runs after every render by default
- The cleanup function (optional) runs before the component unmounts and before the effect runs again
- The dependency array (optional) controls when the effect runs:
- Omitted: Effect runs after every render
- Empty array
[]
: Effect runs only once after the initial render - With dependencies
[a, b]
: Effect runs when any dependency changes
Common Use Cases for useEffect
1. Data Fetching
One of the most common use cases for useEffect
is fetching data from an API:
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Reset states when userId changes
setLoading(true);
setError(null);
// Fetch user data
fetch(`https://api.example.com/users/${userId}`)
.then(response => {
if (!response.ok) throw new Error('Failed to fetch');
return response.json();
})
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, [userId]); // Re-run when userId changes
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
if (!user) return null;
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}
2. Subscriptions and Event Listeners
useEffect
is perfect for setting up and cleaning up subscriptions:
import React, { useState, useEffect } from 'react';
function WindowSizeTracker() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
// Handler to call on window resize
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
// Add event listener
window.addEventListener('resize', handleResize);
// Remove event listener on cleanup
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // Empty array means this effect runs once on mount
return (
<div>
<p>Window width: {windowSize.width}px</p>
<p>Window height: {windowSize.height}px</p>
</div>
);
}
3. DOM Manipulations
When you need to directly interact with the DOM:
import React, { useEffect, useRef } from 'react';
function AutoFocusInput() {
const inputRef = useRef(null);
useEffect(() => {
// Focus the input element after component mounts
inputRef.current.focus();
}, []);
return (
<input
ref={inputRef}
type="text"
placeholder="This input will be focused automatically"
/>
);
}
4. Updating Document Title
A simple but common use case is updating the document title:
import React, { useState, useEffect } from 'react';
function PageTitle({ title }) {
useEffect(() => {
// Update the document title
document.title = title;
// Optional: Restore the original title on unmount
return () => {
document.title = 'React App';
};
}, [title]); // Re-run when title prop changes
return <h1>{title}</h1>;
}
5. Timer Management
Managing timers with proper cleanup:
import React, { useState, useEffect } from 'react';
function CountdownTimer({ seconds }) {
const [timeLeft, setTimeLeft] = useState(seconds);
useEffect(() => {
// Don't start the timer if seconds is zero or negative
if (seconds <= 0) return;
// Reset timer when seconds prop changes
setTimeLeft(seconds);
// Set up the interval
const intervalId = setInterval(() => {
setTimeLeft(prevTime => {
if (prevTime <= 1) {
clearInterval(intervalId);
return 0;
}
return prevTime - 1;
});
}, 1000);
// Clean up the interval on unmount or when seconds changes
return () => clearInterval(intervalId);
}, [seconds]);
return <div>Time remaining: {timeLeft} seconds</div>;
}
6. Syncing with External Systems
Integrating with third-party libraries:
import React, { useEffect, useRef } from 'react';
import Chart from 'chart.js/auto';
function SimpleChart({ data }) {
const chartRef = useRef(null);
const chartInstance = useRef(null);
useEffect(() => {
// If we already have a chart instance, destroy it
if (chartInstance.current) {
chartInstance.current.destroy();
}
// Create new chart
const ctx = chartRef.current.getContext('2d');
chartInstance.current = new Chart(ctx, {
type: 'bar',
data: data,
options: {
responsive: true,
maintainAspectRatio: false
}
});
// Cleanup function
return () => {
if (chartInstance.current) {
chartInstance.current.destroy();
}
};
}, [data]); // Re-run when data changes
return (
<div style={{ width: '300px', height: '200px' }}>
<canvas ref={chartRef} />
</div>
);
}
Advanced Patterns with useEffect
Skipping Effects Conditionally
Sometimes you want to run an effect only under certain conditions:
useEffect(() => {
if (isOnline) {
// Only sync with server when online
syncWithServer(data);
}
}, [isOnline, data]);
Handling Race Conditions in Data Fetching
When fetching data that depends on props or state, you might encounter race conditions:
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
let isMounted = true;
setLoading(true);
fetch(`https://api.example.com/search?q=${query}`)
.then(response => response.json())
.then(data => {
// Only update state if component is still mounted
if (isMounted) {
setResults(data);
setLoading(false);
}
});
// Cleanup function to handle unmounting
return () => {
isMounted = false;
};
}, [query]);
// Component rendering
}
Debouncing with useEffect
For operations that shouldn’t happen too frequently (like search input):
function SearchInput() {
const [searchTerm, setSearchTerm] = useState('');
const [debouncedTerm, setDebouncedTerm] = useState(searchTerm);
// Update searchTerm when input changes
const handleChange = (e) => {
setSearchTerm(e.target.value);
};
// Debounce the search term
useEffect(() => {
const timerId = setTimeout(() => {
setDebouncedTerm(searchTerm);
}, 500); // 500ms delay
return () => {
clearTimeout(timerId);
};
}, [searchTerm]);
// Use debouncedTerm for API calls
useEffect(() => {
if (debouncedTerm) {
// Make API call with debouncedTerm
console.log('Searching for:', debouncedTerm);
}
}, [debouncedTerm]);
return (
<input
type="text"
value={searchTerm}
onChange={handleChange}
placeholder="Search..."
/>
);
}
Real-World Example
Here’s a simple notification system that demonstrates several useEffect patterns:
import React, { useState, useEffect } from 'react';
function NotificationSystem() {
const [notifications, setNotifications] = useState([]);
const [isOnline, setIsOnline] = useState(navigator.onLine);
// Effect for online/offline status
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
// Effect for fetching notifications
useEffect(() => {
if (!isOnline) return;
const fetchNotifications = async () => {
try {
const response = await fetch('/api/notifications');
const data = await response.json();
setNotifications(data);
} catch (error) {
console.error('Failed to fetch notifications:', error);
}
};
fetchNotifications();
// Set up polling
const intervalId = setInterval(fetchNotifications, 60000);
return () => clearInterval(intervalId);
}, [isOnline]);
// Effect for notification sound
useEffect(() => {
if (notifications.length > 0 && notifications.some(n => !n.read)) {
// Play notification sound
const audio = new Audio('/notification-sound.mp3');
audio.play().catch(e => console.log('Audio play failed:', e));
}
}, [notifications]);
return (
<div className="notification-panel">
<h2>Notifications {!isOnline && '(offline)'}</h2>
{notifications.length === 0 ? (
<p>No new notifications</p>
) : (
<ul>
{notifications.map(notification => (
<li key={notification.id} className={notification.read ? 'read' : 'unread'}>
{notification.message}
</li>
))}
</ul>
)}
</div>
);
}
Common Mistakes and Pitfalls
1. Missing Dependencies
Forgetting to include all dependencies used inside the effect:
// Incorrect
useEffect(() => {
console.log(`User ${userName} logged in`);
}, []); // Missing userName in dependencies
// Correct
useEffect(() => {
console.log(`User ${userName} logged in`);
}, [userName]);
2. Infinite Loops
Creating an infinite loop by updating state in an effect without proper dependencies:
// This will cause an infinite loop!
useEffect(() => {
setCount(count + 1);
}); // No dependency array
// Fixed version - only run once
useEffect(() => {
setCount(c => c + 1);
}, []); // Empty dependency array
3. Forgetting Cleanup
Not cleaning up subscriptions, timers, or event listeners:
// Incorrect - memory leak
useEffect(() => {
const subscription = someExternalAPI.subscribe();
// No cleanup
}, []);
// Correct
useEffect(() => {
const subscription = someExternalAPI.subscribe();
return () => {
subscription.unsubscribe();
};
}, []);
Interview Tips
Explain the purpose: useEffect is for side effects that can’t be done during rendering.
Discuss the dependency array: Be ready to explain how the dependency array controls when effects run.
Highlight cleanup: Emphasize the importance of the cleanup function for preventing memory leaks.
Compare with lifecycle methods: Be prepared to compare useEffect with componentDidMount, componentDidUpdate, and componentWillUnmount.
Mention common use cases: Data fetching, subscriptions, DOM manipulations, and timers are all good examples.
Address performance concerns: Discuss how to optimize effects with proper dependencies and conditional execution.
Talk about race conditions: Show your understanding of handling asynchronous operations properly.
Discuss the rules: Remember that useEffect follows the Rules of Hooks - it can’t be called conditionally.
Test Your Knowledge
Take a quick quiz to test your understanding of this topic.