Understanding React Hooks: useEffect vs useMemo vs useCallback
A comprehensive guide to understanding when and why to use useEffect, useMemo, and useCallback in React applications.
Understanding React Hooks: useEffect vs useMemo vs useCallback
React hooks are powerful tools that help us manage state and side effects in functional components. However, understanding when to use each hook can be confusing, especially for developers new to React. In this post, we'll explore the key differences between useEffect, useMemo, and useCallback, and learn when to use each one.
The Three Hooks Explained
useEffect - Side Effects and Lifecycle
useEffect is used for side effects that need to happen after rendering. It's the functional component equivalent of lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount.
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// This runs after every render
const fetchUser = async () => {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
setUser(userData);
} catch (error) {
console.error('Failed to fetch user:', error);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]); // Only re-run when userId changes
if (loading) return <div>Loading...</div>;
return <div>{user?.name}</div>;
}
When to use useEffect:
- API calls and data fetching
- Setting up subscriptions
- Manually changing the DOM
- Timers and intervals
- Cleanup operations
useMemo - Expensive Calculations
useMemo memoizes the result of a computation and only recalculates when its dependencies change. It's useful for expensive calculations that you don't want to run on every render.
import { useState, useMemo } from 'react';
function ExpensiveComponent({ items, filter }) {
const [searchTerm, setSearchTerm] = useState('');
// This expensive calculation only runs when items, filter, or searchTerm change
const filteredItems = useMemo(() => {
console.log('Filtering items...'); // This won't log on every render
return items
.filter(item => item.category === filter)
.filter(item => item.name.toLowerCase().includes(searchTerm.toLowerCase()))
.sort((a, b) => a.name.localeCompare(b.name));
}, [items, filter, searchTerm]);
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search items..."
/>
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
When to use useMemo:
- Expensive calculations or transformations
- Filtering and sorting large datasets
- Creating objects or arrays that are passed as props
- Preventing unnecessary re-renders of child components
useCallback - Function Memoization
useCallback memoizes a function and returns the same function reference when its dependencies haven't changed. This prevents child components from re-rendering unnecessarily when the function is passed as a prop.
import { useState, useCallback } from 'react';
function ParentComponent() {
const [count, setCount] = useState(0);
const [items, setItems] = useState([]);
// Without useCallback, this function is recreated on every render
const handleAddItem = useCallback((newItem) => {
setItems(prev => [...prev, newItem]);
}, []); // Empty dependency array means this function never changes
// This function depends on count, so it will be recreated when count changes
const handleIncrement = useCallback(() => {
setCount(prev => prev + 1);
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={handleIncrement}>Increment</button>
<ItemList items={items} onAddItem={handleAddItem} />
</div>
);
}
// Child component that receives the memoized function
const ItemList = React.memo(({ items, onAddItem }) => {
console.log('ItemList rendered'); // This won't log unnecessarily
return (
<div>
{items.map(item => (
<div key={item.id}>{item.name}</div>
))}
<button onClick={() => onAddItem({ id: Date.now(), name: 'New Item' })}>
Add Item
</button>
</div>
);
});
When to use useCallback:
- Functions passed as props to child components
- Functions used in dependency arrays of other hooks
- Event handlers that are expensive to recreate
- Preventing unnecessary re-renders of memoized components
Key Differences Summary
| Hook | Purpose | When to Use | Dependencies |
|---|---|---|---|
useEffect | Side effects after render | API calls, subscriptions, DOM manipulation | Array of values to watch |
useMemo | Memoize expensive calculations | Complex computations, filtering, sorting | Array of values that affect the calculation |
useCallback | Memoize function references | Functions passed as props, event handlers | Array of values the function depends on |
Common Pitfalls and Best Practices
1. Don't Overuse useMemo and useCallback
// ❌ Unnecessary - simple calculations don't need memoization
const doubled = useMemo(() => count * 2, [count]);
// ✅ Just use regular calculation
const doubled = count * 2;
2. Be Careful with Dependency Arrays
// ❌ Missing dependency - will cause stale closure
useEffect(() => {
fetchData(userId);
}, []); // Missing userId dependency
// ✅ Correct dependency array
useEffect(() => {
fetchData(userId);
}, [userId]);
3. Don't Memoize Everything
// ❌ Over-memoization - objects are recreated anyway
const config = useMemo(() => ({
theme: 'dark',
language: 'en'
}), []);
// ✅ Just create the object directly
const config = { theme: 'dark', language: 'en' };
Performance Considerations
When Memoization Helps
function ExpensiveParent({ data }) {
// This expensive calculation only runs when data changes
const processedData = useMemo(() => {
return data.map(item => ({
...item,
processed: expensiveTransformation(item)
}));
}, [data]);
// This function reference stays stable
const handleClick = useCallback((id) => {
console.log('Clicked item:', id);
}, []);
return (
<div>
{processedData.map(item => (
<ExpensiveChild
key={item.id}
data={item}
onClick={handleClick}
/>
))}
</div>
);
}
When Memoization Doesn't Help
function SimpleComponent({ name }) {
// ❌ Unnecessary - simple string concatenation
const greeting = useMemo(() => `Hello, ${name}!`, [name]);
// ❌ Unnecessary - simple function
const handleClick = useCallback(() => {
alert(`Hello, ${name}!`);
}, [name]);
return <button onClick={handleClick}>{greeting}</button>;
}
Real-World Example: Search and Filter
Let's see all three hooks working together in a practical example:
import { useState, useEffect, useMemo, useCallback } from 'react';
function ProductSearch() {
const [products, setProducts] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
const [category, setCategory] = useState('all');
const [loading, setLoading] = useState(true);
// useEffect for data fetching
useEffect(() => {
const fetchProducts = async () => {
setLoading(true);
try {
const response = await fetch('/api/products');
const data = await response.json();
setProducts(data);
} catch (error) {
console.error('Failed to fetch products:', error);
} finally {
setLoading(false);
}
};
fetchProducts();
}, []);
// useMemo for expensive filtering and sorting
const filteredProducts = useMemo(() => {
return products
.filter(product =>
category === 'all' || product.category === category
)
.filter(product =>
product.name.toLowerCase().includes(searchTerm.toLowerCase())
)
.sort((a, b) => a.price - b.price);
}, [products, searchTerm, category]);
// useCallback for stable function reference
const handleProductClick = useCallback((productId) => {
// Navigate to product detail page
window.location.href = `/products/${productId}`;
}, []);
if (loading) return <div>Loading products...</div>;
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search products..."
/>
<select value={category} onChange={(e) => setCategory(e.target.value)}>
<option value="all">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
<div>
{filteredProducts.map(product => (
<ProductCard
key={product.id}
product={product}
onClick={handleProductClick}
/>
))}
</div>
</div>
);
}
const ProductCard = React.memo(({ product, onClick }) => {
return (
<div onClick={() => onClick(product.id)}>
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
);
});
Conclusion
Understanding when to use useEffect, useMemo, and useCallback is crucial for writing efficient React applications:
- useEffect: Use for side effects that need to happen after rendering
- useMemo: Use for expensive calculations that you want to cache
- useCallback: Use for functions that you want to keep stable across renders
Remember that premature optimization can be counterproductive. Start with simple, readable code and add memoization only when you have performance issues and can measure the improvement.
The key is to understand what each hook does and use them judiciously to solve specific problems, not as a blanket solution for all performance concerns.
Post Details
Navigation
Related posts
Why We Need forwardRef in React: A Complete Guide
Understanding forwardRef in React: when to use it, why it's necessary, and practical examples for building reusable components.
Read more →Building a Chatbot with RAG: How Retrieval Meets the LLM
A practical look at Retrieval-Augmented Generation: embeddings, vector search, and how to wire them to an LLM—plus how this portfolio implements the same pattern with Next.js, Supabase pgvector, and Hugging Face.
Read more →Essential Security Practices to Protect Your Web Applications
Practical, easy-to-apply security improvements for any online project — from security headers, rate limiting, login protection, to safe file uploads and more.
Read more →