React
March 1, 2025
React useEffect: The Complete Guide to DOs and DON'Ts
Master React's most powerful and misunderstood hook with practical examples and performance optimization techniques
SHARE

React useEffect: The Complete Guide to DOs and DON'Ts
Introduction
The useEffect
hook is one of React's most powerful features, yet it's also one of the most misunderstood. Having worked on several complex React applications, I've witnessed firsthand how improper use of useEffect
can lead to subtle bugs, memory leaks, and performance issues that are difficult to diagnose.
In this comprehensive guide, I'll share the best practices, common pitfalls, and optimization techniques I've learned from building production-grade React applications.

Understanding useEffect: The Mental Model
Before diving into specific patterns, it's crucial to understand what useEffect
actually does. Many React developers struggle with effects because they approach them with an incorrect mental model.
The key insight: useEffect
is not just for "side effects" in the traditional programming sense. It's for synchronizing your component with external systems.
That's it. That's the whole purpose of useEffect
. When you think about effects in terms of synchronization, many of the common patterns and pitfalls become clearer.
jsx
function ProfilePage({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// This effect synchronizes the component with an external system (API)
async function fetchUser() {
const response = await fetch(/api/users/${userId}
);
const userData = await response.json();
setUser(userData);
}
fetchUser();
}, [userId]); // Re-synchronize when userId changes
// ...
}
With this mental model in place, let's explore specific DOs and DON'Ts.
The DO's of useEffect
DO: Use a Dependency Array
Always include a dependency array, even if it's empty. This is not just about optimization—it's about explicitly stating your synchronization intent.
jsx
// ✅ Good: Component synchronizes once after mount
useEffect(() => {
document.title = 'Welcome to the app';
}, []); // Empty dependency array = run once after mount
// ✅ Good: Component re-synchronizes when count changes
useEffect(() => {
document.title = You clicked ${count} times
;
}, [count]); // Dependency array with values to track
// ❌ Bad: Missing dependency array = run after every render
useEffect(() => {
document.title = You clicked ${count} times
;
});
DO: Include All Dependencies Used Inside
React's dependency array is not optional in terms of what you include. Include every value from the component scope that's used inside the effect.
jsx
// ✅ Good: All dependencies are properly declared
function SearchResults({ query, sortOrder }) {
const [results, setResults] = useState([]);
useEffect(() => {
async function fetchResults() {
const response = await fetch(/api/search?q=${query}&sort=${sortOrder}
);
const data = await response.json();
setResults(data);
}
fetchResults();
}, [query, sortOrder]); // Both query and sortOrder are dependencies
// ...
}
DO: Clean Up After Your Effects
Many effects create resources that need to be disposed of when the component unmounts or before the effect runs again. Always return a cleanup function when appropriate.
jsx
// ✅ Good: Properly cleaning up subscriptions
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
connection.on('message', (message) => {
setMessages(prev => [...prev, message]);
});
// Return a cleanup function
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}
DO: Use Multiple Effects for Unrelated Logic
Separate concerns by using multiple useEffect
hooks. This makes your code more maintainable and easier to understand.
jsx
// ✅ Good: Separating unrelated effects
function UserProfile({ userId }) {
// Effect 1: Fetch user data
useEffect(() => {
fetchUserData(userId).then(data => setUserData(data));
}, [userId]);
// Effect 2: Track page views
useEffect(() => {
logPageView('user-profile');
}, []);
// Effect 3: Set up keyboard shortcuts
useEffect(() => {
const handleKeyPress = (event) => {
if (event.key === 'Escape') closeProfile();
};
window.addEventListener('keydown', handleKeyPress);
return () => window.removeEventListener('keydown', handleKeyPress);
}, []);
// ...
}
DO: Use the Functional Update Form for State Updates
When updating state based on previous state inside an effect, use the functional update pattern to avoid stale closures.
jsx
// ✅ Good: Using functional updates to avoid stale state
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(prevCount => prevCount + 1); // Using functional update
}, 1000);
return () => clearInterval(interval);
}, []); // Empty dependency array is fine with functional updates
// ...
}
The DON'Ts of useEffect
DON'T: Ignore the Exhaustive-deps ESLint Rule
The React team created the exhaustive-deps ESLint rule for a reason. Ignoring it with eslint-disable comments often leads to subtle bugs.
jsx
// ❌ Bad: Ignoring the eslint rule
useEffect(() => {
document.title = ${count} - ${user.name}
;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [count]); // Missing user dependency
// ✅ Good: Including all dependencies
useEffect(() => {
document.title = ${count} - ${user.name}
;
}, [count, user.name]);
DON'T: Use Effects for Data Transformations
If you're computing some data solely based on current props and state, you don't need an effect.
jsx
// ❌ Bad: Using effect for data transformation
function ProductList({ products }) {
const [filteredProducts, setFilteredProducts] = useState([]);
useEffect(() => {
setFilteredProducts(
products.filter(product => product.inStock)
);
}, [products]);
// ...
}
// ✅ Good: Computing values directly during render
function ProductList({ products }) {
const filteredProducts = useMemo(() => {
return products.filter(product => product.inStock);
}, [products]);
// or even simpler for this case:
// const filteredProducts = products.filter(product => product.inStock);
// ...
}
DON'T: Create Race Conditions with Async Effects
Async operations in effects can lead to race conditions if you're not careful.
jsx
// ❌ Bad: Potential race condition
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
async function fetchResults() {
const response = await fetch(/api/search?q=${query}
);
const data = await response.json();
setResults(data); // Might set results for an outdated query
}
fetchResults();
}, [query]);
// ...
}
// ✅ Good: Preventing race conditions
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
let isMounted = true;
async function fetchResults() {
const response = await fetch(/api/search?q=${query}
);
const data = await response.json();
if (isMounted) setResults(data);
}
fetchResults();
return () => {
isMounted = false;
};
}, [query]);
// ...
}
DON'T: Over-synchronize with Non-React Systems
Constantly synchronizing with non-React systems on every render can cause performance issues and potential infinite loops.
jsx
// ❌ Bad: Over-synchronizing localStorage on every render
function SavedSettings() {
const [isDarkMode, setIsDarkMode] = useState(false);
// This will cause localStorage to be updated on every render
useEffect(() => {
localStorage.setItem('isDarkMode', JSON.stringify(isDarkMode));
});
// ...
}
// ✅ Good: Only synchronize when isDarkMode changes
function SavedSettings() {
const [isDarkMode, setIsDarkMode] = useState(false);
useEffect(() => {
localStorage.setItem('isDarkMode', JSON.stringify(isDarkMode));
}, [isDarkMode]);
// ...
}
DON'T: Use Effects to Manage Component Lifecycle
Trying to mimic class component lifecycle methods with effects leads to convoluted code. Instead, think in terms of synchronization.
jsx
// ❌ Bad: Trying to mimic componentDidMount, componentDidUpdate, etc.
function ProfilePage({ userId }) {
const [isFirstRender, setIsFirstRender] = useState(true);
useEffect(() => {
if (isFirstRender) {
// "componentDidMount" logic
loadDefaultData();
setIsFirstRender(false);
} else {
// "componentDidUpdate" logic
loadUserSpecificData(userId);
}
}, [userId, isFirstRender]);
// ...
}
// ✅ Good: Thinking in terms of synchronization
function ProfilePage({ userId }) {
// Effect for one-time initialization
useEffect(() => {
loadDefaultData();
}, []);
// Separate effect that synchronizes with userId
useEffect(() => {
if (userId) {
loadUserSpecificData(userId);
}
}, [userId]);
// ...
}
Advanced Techniques
Technique 1: Custom Hooks for Common Patterns
Extract common effect patterns into custom hooks for reusability and abstraction.
jsx
// Custom hook for API fetching with loading/error states
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true;
setLoading(true);
async function fetchData() {
try {
const response = await fetch(url);
if (!response.ok) throw new Error('Network response was not ok');
const result = await response.json();
if (isMounted) {
setData(result);
setError(null);
}
} catch (err) {
if (isMounted) setError(err.message);
} finally {
if (isMounted) setLoading(false);
}
}
fetchData();
return () => {
isMounted = false;
};
}, [url]);
return { data, loading, error };
}
// Usage in component is now much cleaner
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(/api/users/${userId}
);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <div>{user.name}</div>;
}
Technique 2: The Decoupling Pattern
For complex effects with many dependencies, you can decouple the effect from its dependencies using refs.
jsx
// Decoupling using refs for complex effects
function ChatRoom({ roomId, userId, theme }) {
// Store current values in refs to access in effect cleanup
const roomIdRef = useRef(roomId);
const userIdRef = useRef(userId);
// Update refs when props change
useEffect(() => {
roomIdRef.current = roomId;
userIdRef.current = userId;
});
// Main chat connection effect
useEffect(() => {
// Connect to chat service
const connection = createChatConnection(roomId, userId);
connection.connect();
return () => {
// Use current refs for cleanup to ensure latest values
connection.disconnect(roomIdRef.current, userIdRef.current);
};
}, [roomId, userId]); // Only recreate connection when these change
// Theme-related effect is separate and doesn't affect chat connection
useEffect(() => {
document.body.dataset.theme = theme;
}, [theme]);
// ...
}
Technique 3: Effect Orchestration
For complex workflows that involve multiple sequential effects, you can orchestrate them with state.
jsx
// Effect orchestration for multi-step processes
function OrderCheckout() {
const [orderState, setOrderState] = useState('idle'); // idle, validating, processing, success, error
const [orderDetails, setOrderDetails] = useState(null);
const [error, setError] = useState(null);
// Step 1: Validate order when user submits
function handleSubmit(details) {
setOrderDetails(details);
setOrderState('validating');
}
// Effect for validation step
useEffect(() => {
if (orderState !== 'validating') return;
async function validateOrder() {
try {
const validationResult = await validateOrderAPI(orderDetails);
if (validationResult.valid) {
setOrderState('processing');
} else {
setError(validationResult.errors);
setOrderState('error');
}
} catch (err) {
setError('Validation failed');
setOrderState('error');
}
}
validateOrder();
}, [orderState, orderDetails]);
// Effect for processing step
useEffect(() => {
if (orderState !== 'processing') return;
async function processOrder() {
try {
await processOrderAPI(orderDetails);
setOrderState('success');
} catch (err) {
setError('Processing failed');
setOrderState('error');
}
}
processOrder();
}, [orderState, orderDetails]);
// Render different UI based on orderState
// ...
}
Performance Optimizations
Optimization 1: Throttling/Debouncing Effect Dependencies
For effects with rapidly changing dependencies, throttle or debounce to limit the frequency of effect execution.
jsx
// Using debounce for search input
function SearchComponent() {
const [query, setQuery] = useState('');
const [debouncedQuery, setDebouncedQuery] = useState(query);
// Update debounced value after delay
useEffect(() => {
const timer = setTimeout(() => setDebouncedQuery(query), 500);
return () => clearTimeout(timer);
}, [query]);
// Only fetch results when debouncedQuery changes
useEffect(() => {
if (debouncedQuery) {
fetchSearchResults(debouncedQuery);
}
}, [debouncedQuery]);
return (
<input
type="text"
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
);
}
Optimization 2: Memoization for Complex Dependencies
When your effect depends on complex objects or arrays, use useMemo
to prevent unnecessary effect triggers.
jsx
// Using useMemo to stabilize complex dependencies
function ProductFilter({ products, filters }) {
// Memoize the filtered products to stabilize this dependency
const filteredProducts = useMemo(() => {
return products.filter(product => {
return Object.entries(filters).every(([key, value]) => {
return product[key] === value;
});
});
}, [products, filters]);
// This effect won't run unless filteredProducts actually changes
useEffect(() => {
logAnalytics('filtered-products-view', { count: filteredProducts.length });
}, [filteredProducts]);
// ...
}
Optimization 3: Lazy Initialization
For expensive initial setup, use lazy initialization to improve first render performance.
jsx
// Lazy initialization for localStorage
function SavedSettings() {
// Instead of reading from localStorage on every render
const [isDarkMode, setIsDarkMode] = useState(() => {
// This function runs only on first render
try {
const saved = localStorage.getItem('isDarkMode');
return saved ? JSON.parse(saved) : false;
} catch (e) {
return false;
}
});
// Save to localStorage when value changes
useEffect(() => {
localStorage.setItem('isDarkMode', JSON.stringify(isDarkMode));
}, [isDarkMode]);
// ...
}
Common useEffect Patterns
Pattern 1: Data Fetching
jsx
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true;
setLoading(true);
async function fetchProduct() {
try {
const response = await fetch(/api/products/${productId}
);
const data = await response.json();
if (isMounted) {
setProduct(data);
setError(null);
}
} catch (err) {
if (isMounted) setError('Failed to fetch product');
} finally {
if (isMounted) setLoading(false);
}
}
fetchProduct();
return () => {
isMounted = false;
};
}, [productId]);
// ...
}
Pattern 2: Event Subscriptions
jsx
function WindowSize() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
}
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // Empty array ensures the effect runs only once
// ...
}
Pattern 3: Integrating Third-Party Libraries
jsx
function Map({ center, zoom }) {
const mapRef = useRef(null);
const [map, setMap] = useState(null);
// Initialize map
useEffect(() => {
if (mapRef.current && !map) {
const newMap = new GoogleMap(mapRef.current, {
center,
zoom
});
setMap(newMap);
}
}, [center, zoom, map]);
// Update map when props change
useEffect(() => {
if (map) {
map.setCenter(center);
map.setZoom(zoom);
}
}, [center, zoom, map]);
return <div ref={mapRef} style={{ height: '400px' }} />;
}
When Not to Use useEffect
Despite its power, there are cases where useEffect
isn't the right tool:
- Transforming Data for Rendering: Use regular variables or
useMemo
instead. - Managing State Based on Props: Derive state directly in your render function.
- Handling User Events: Use event handlers, not effects.
- Running Code Only Once During Development: Use a module-level variable instead.
Conclusion
Mastering useEffect
is key to writing maintainable and efficient React applications. By understanding its true purpose—synchronizing your component with external systems—and following the DOs and DON'Ts outlined in this guide, you can avoid common pitfalls and unlock the full potential of React's hooks system.
Remember, the best effects are often the ones you don't need to write at all. Always consider if there's a more direct way to achieve your goal before reaching for useEffect
.
Further Resources
About the Author: Ajithkumar R is a full-stack developer specializing in React and modern JavaScript frameworks. He has worked on multiple large-scale React applications and enjoys sharing his knowledge and experience with the community.
Reader Comments
No comments yet.
Share Your Experience
Give a star rating and let me know your impressions.