useEffect vs useCallback vs useMemo: The Complete Guide


React hooks can be confusing, especially when you're trying to figure out which one to use when. Let's break down these three essential hooks with simple explanations, practical examples, and easy-to-remember rules.
๐ฏ Quick Reference Table
Hook | Purpose | Returns | When to Use |
useEffect | Handle side effects | Nothing | API calls, subscriptions, DOM manipulation |
useCallback | Memoize functions | Memoized function | Prevent unnecessary re-renders of child components |
useMemo | Memoize values | Memoized value | Expensive calculations, complex object creation |
๐ useEffect: The Side Effect Manager
What it does: Handles side effects in your components (things that happen "on the side" of rendering).
Think of it as: Your component's event listener for lifecycle moments.
Basic Syntax
useEffect(() => {
// Side effect code here
return () => {
// Cleanup code (optional)
};
}, [dependencies]); // Dependencies array
When to Use useEffect
โ Perfect for:
API calls and data fetching
Setting up subscriptions (WebSocket, event listeners)
DOM manipulation (focus, scroll position)
Cleanup tasks (removing event listeners, canceling requests)
Running code after render (analytics, logging)
Example: Data Fetching
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
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]); // Re-run when userId changes
if (loading) return <div>Loading...</div>;
return <div>Hello, {user?.name}!</div>;
}
Dependency Array Rules
No array: Runs after every render
Empty array
[]
: Runs once after initial renderWith dependencies
[value]
: Runs when dependencies change
๐ useCallback: The Function Memoizer
What it does: Returns a memoized version of a function that only changes when its dependencies change.
Think of it as: A way to keep the same function reference across re-renders.
Basic Syntax
const memoizedCallback = useCallback(() => {
// Function code here
}, [dependencies]);
When to Use useCallback
โ Perfect for:
Passing functions to child components (prevents unnecessary re-renders)
Functions used in other hooks' dependencies
Event handlers in lists (optimization)
Expensive function creation
Example: Preventing Child Re-renders
function TodoApp() {
const [todos, setTodos] = useState([]);
const [filter, setFilter] = useState('all');
// Without useCallback, this function is recreated on every render
// causing TodoList to re-render unnecessarily
const addTodo = useCallback((text) => {
setTodos(prev => [...prev, { id: Date.now(), text, completed: false }]);
}, []); // No dependencies - function never changes
const deleteTodo = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
return (
<div>
<TodoForm onAddTodo={addTodo} />
<TodoList todos={todos} onDeleteTodo={deleteTodo} />
<FilterButtons filter={filter} setFilter={setFilter} />
</div>
);
}
// Child component - only re-renders when todos actually change
const TodoList = React.memo(({ todos, onDeleteTodo }) => {
console.log('TodoList rendered'); // This won't log unnecessarily
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
<button onClick={() => onDeleteTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
);
});
๐พ useMemo: The Value Memoizer
What it does: Returns a memoized value that only recalculates when its dependencies change.
Think of it as: A way to cache expensive calculations.
Basic Syntax
const memoizedValue = useMemo(() => {
return expensiveCalculation(a, b);
}, [a, b]);
When to Use useMemo
โ Perfect for:
Expensive calculations (complex math, data processing)
Filtering/sorting large lists
Creating complex objects (to maintain reference equality)
Derived state (values computed from other state)
Example: Expensive Calculation
function ProductList({ products, searchTerm, category }) {
// Expensive filtering operation - only recalculate when inputs change
const filteredProducts = useMemo(() => {
console.log('Filtering products...'); // Only logs when dependencies change
return products
.filter(product => {
const matchesSearch = product.name
.toLowerCase()
.includes(searchTerm.toLowerCase());
const matchesCategory = category === 'all' || product.category === category;
return matchesSearch && matchesCategory;
})
.sort((a, b) => a.name.localeCompare(b.name));
}, [products, searchTerm, category]);
// Complex object creation - prevents unnecessary re-renders
const listConfig = useMemo(() => ({
showImages: true,
showPrices: true,
layout: 'grid'
}), []); // Empty dependency - object never changes
return <ProductGrid products={filteredProducts} config={listConfig} />;
}
๐ค Common Confusion Points
useCallback vs useMemo
// These are equivalent:
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
const memoizedCallback = useMemo(() => {
return () => doSomething(a, b);
}, [a, b]);
// But useCallback is cleaner for functions
When NOT to Use These Hooks
โ Don't use useCallback/useMemo for:
Simple calculations (more overhead than benefit)
Primitive values (strings, numbers, booleans are cheap to compare)
Every function/value (premature optimization)
// โ Unnecessary - primitive values are cheap
const doubled = useMemo(() => count * 2, [count]);
// โ
Better - just calculate directly
const doubled = count * 2;
๐ฏ Easy Memory Tricks
The "3 E's" Rule
useEffect: Effects (side effects, external operations)
useCallback: Event handlers (functions passed to children)
useMemo: Expensive operations (calculations, processing)
When in Doubt
Need to do something after render? โ
useEffect
Passing a function to a child component? โ
useCallback
Have an expensive calculation? โ
useMemo
Not sure if it's expensive? โ Profile first, optimize later
๐ Best Practices
1. Start Without Optimization
// Start simple
function Component({ items }) {
const expensiveValue = calculateSomething(items);
return <div>{expensiveValue}</div>;
}
// Add optimization only if needed
function Component({ items }) {
const expensiveValue = useMemo(() => calculateSomething(items), [items]);
return <div>{expensiveValue}</div>;
}
2. Use React.memo with useCallback
// Combine for maximum benefit
const ExpensiveChild = React.memo(({ onClick, data }) => {
// This component only re-renders when props actually change
return <div onClick={onClick}>{data}</div>;
});
function Parent() {
const handleClick = useCallback(() => {
// Handle click
}, []);
return <ExpensiveChild onClick={handleClick} data="static" />;
}
3. Profile Before Optimizing
Use React DevTools Profiler to identify actual performance bottlenecks before adding memoization.
๐ Summary
Scenario | Use This Hook | Why |
Fetch data when component mounts | useEffect | Side effect after render |
Subscribe to WebSocket | useEffect | Side effect with cleanup |
Pass function to child component | useCallback | Prevent child re-renders |
Filter large list | useMemo | Expensive calculation |
Create complex configuration object | useMemo | Maintain reference equality |
Remember: Measure first, optimize second. These hooks are powerful tools, but they're not always necessary. Start with simple code and add optimization when you have a proven performance issue.
Happy coding! ๐
Subscribe to my newsletter
Read articles from Omkar Patil directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
