useCallback and useMemo in React: What They Do, How They Work & Best Use Cases [Visual Codepen Example]
React’s declarative nature makes building UIs intuitive, but as applications grow, performance bottlenecks can emerge—especially around unnecessary re-renders and expensive computations. Two hooks, useCallback and useMemo, are designed to optimize these scenarios by leveraging memoization (caching values to avoid redundant work).
If you’ve ever wondered why a child component re-renders unexpectedly or why a heavy calculation slows down your app on every state change, this guide will demystify useCallback and useMemo. We’ll break down how they work, their key differences, real-world use cases, and even walk through a live Codepen example to see them in action.
Table of Contents#
- What is React Re-rendering?
- Understanding
useCallback - Understanding
useMemo useCallbackvs.useMemo: Key Differences- Visual Codepen Example
- Best Practices
- Common Pitfalls to Avoid
- When Not to Use
useCallbackoruseMemo - Conclusion
- References
What is React Re-rendering?#
Before diving into useCallback and useMemo, it’s critical to understand React’s re-rendering behavior. A React component re-renders when:
- Its own
statechanges. - Its parent component re-renders (unless the child is optimized with
React.memoor similar). - Its
propschange (based on reference equality, not value equality).
By default, React re-renders components frequently to keep the UI in sync with state/props. While this is efficient for small apps, unnecessary re-renders or repeated expensive computations can degrade performance. This is where memoization hooks come in.
Understanding useCallback#
What is useCallback?#
useCallback is a React hook that memoizes functions. In React, functions defined inside a component are recreated on every render. If you pass such a function as a prop to a child component, the child may re-render unnecessarily—even if the function’s logic hasn’t changed—because the function reference is new each time.
useCallback solves this by returning a memoized version of the function that only changes if one of its dependencies updates. This preserves the function reference across re-renders, preventing unnecessary child re-renders.
How useCallback Works#
Syntax:
const memoizedCallback = useCallback(
() => {
// Function logic here
},
[dependencies], // Array of values the function depends on
); - First argument: The function you want to memoize.
- Second argument: A dependency array (like
useEffect). The memoized function updates only when a dependency changes. - Return value: The memoized function.
Example: Without useCallback#
Consider a parent component passing a callback to a child component wrapped in React.memo (which prevents re-renders if props are unchanged):
import { memo, useState } from 'react';
// Memoized child component
const Child = memo(({ onClick }) => {
console.log("Child re-rendered");
return <button onClick={onClick}>Click me</button>;
});
// Parent component
const Parent = () => {
const [count, setCount] = useState(0);
// This function is recreated on every render!
const handleClick = () => {
console.log("Button clicked");
};
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
<Child onClick={handleClick} />
</div>
);
}; Problem: Even though Child is memoized, clicking "Increment Count" (which updates count) causes Parent to re-render. Since handleClick is recreated on every render, Child receives a new onClick prop and re-renders unnecessarily ("Child re-rendered" logs on every count increment).
Example: With useCallback#
Add useCallback to memoize handleClick:
import { memo, useState, useCallback } from 'react';
const Child = memo(({ onClick }) => {
console.log("Child re-rendered");
return <button onClick={onClick}>Click me</button>;
});
const Parent = () => {
const [count, setCount] = useState(0);
// Memoize handleClick: only re-created if dependencies change (none here)
const handleClick = useCallback(() => {
console.log("Button clicked");
}, []); // Empty dependency array: function never changes
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
<Child onClick={handleClick} />
</div>
);
}; Solution: Now, handleClick is memoized and only created once (since the dependency array is empty). When Parent re-renders (due to count changing), Child receives the same onClick reference and does not re-render ("Child re-rendered" only logs once, on initial render).
Common Use Cases for useCallback#
-
Passing Callbacks to Memoized Child Components (as in the example above).
-
Callbacks in Hook Dependencies (e.g.,
useEffect): If a function is a dependency ofuseEffect, memoizing it withuseCallbackprevents the effect from re-running unnecessarily.useEffect(() => { const interval = setInterval(handleTick, 1000); return () => clearInterval(interval); }, [handleTick]); // Without useCallback, handleTick changes on every render, causing the effect to re-run. -
Optimizing Custom Hooks that return functions (e.g., a
useApihook returning afetchDatafunction).
Understanding useMemo#
What is useMemo?#
While useCallback memoizes functions, useMemo memoizes the result of expensive calculations. In React, expensive operations (e.g., sorting a large list, filtering data, or complex computations) run on every render by default, which can slow down your app.
useMemo solves this by caching the result of the calculation and only re-computing it when one of its dependencies changes. This ensures expensive work runs only when necessary.
How useMemo Works#
Syntax:
const memoizedValue = useMemo(
() => {
// Expensive calculation here
return result;
},
[dependencies], // Re-compute only if dependencies change
); - First argument: A function that performs the expensive calculation and returns a value.
- Second argument: A dependency array. The memoized value updates only when a dependency changes.
- Return value: The memoized result of the calculation.
Example: Without useMemo#
Consider a component that filters a large list of items on every render:
import { useState } from 'react';
const ExpensiveComponent = () => {
const [searchTerm, setSearchTerm] = useState("");
const [items] = useState(Array.from({ length: 10000 }, (_, i) => `Item ${i}`)); // Large list
// Expensive: runs on EVERY render (even if searchTerm doesn't change!)
const filteredItems = items.filter(item =>
item.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div>
<input
type="text"
placeholder="Search..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<ul>
{filteredItems.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}; Problem: Filtering 10,000 items is computationally expensive. Even if searchTerm doesn’t change (e.g., the user is typing but deletes the input, leaving searchTerm empty), filteredItems re-runs on every render, causing lag.
Example: With useMemo#
Add useMemo to memoize the filtered result:
import { useState, useMemo } from 'react';
const ExpensiveComponent = () => {
const [searchTerm, setSearchTerm] = useState("");
const [items] = useState(Array.from({ length: 10000 }, (_, i) => `Item ${i}`));
// Memoize the filtered result: only re-compute when searchTerm or items change
const filteredItems = useMemo(() => {
console.log("Filtering items..."); // Logs only when searchTerm changes
return items.filter(item =>
item.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [searchTerm, items]); // Re-run only if searchTerm or items change
return (
<div>
<input
type="text"
placeholder="Search..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<ul>
{filteredItems.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}; Solution: filteredItems now re-computes only when searchTerm or items changes. Typing in the search box triggers the filter, but if searchTerm is unchanged (e.g., re-renders from other state changes), the cached result is reused, improving performance.
Common Use Cases for useMemo#
- Expensive Calculations (sorting, filtering large datasets, mathematical computations).
- Memoizing Object/Array Props (to prevent memoized child components from re-rendering). For example:
// Without useMemo: new object on every render → child re-renders const user = { name: "Alice", age: 30 }; // With useMemo: memoized object → child only re-renders if name/age change const user = useMemo(() => ({ name: "Alice", age: 30 }), [name, age]); - Derived State (state computed from existing state/props that’s expensive to generate).
useCallback vs. useMemo: Key Differences#
| Feature | useCallback | useMemo |
|---|---|---|
| Purpose | Memoizes functions | Memoizes values (results of calculations) |
| Returns | A memoized function | A memoized value |
| Use Case | Preventing re-renders of memoized children (via stable function references). | Avoiding redundant expensive calculations. |
| Analogy | "Cache this function so it doesn’t change." | "Cache the result of this calculation so it doesn’t re-run." |
Simpler way to remember:
- Use
useCallbackwhen you need a stable function reference. - Use
useMemowhen you need a stable value from an expensive calculation.
Visual Codepen Example#
Let’s combine both hooks in a live example to see their impact. We’ll build a component with:
- A memoized child component that receives a callback and an object prop.
- An expensive calculation (filtering a large list).
Codepen Link: useCallback & useMemo Demo
Code Walkthrough#
import { useState, useCallback, useMemo, memo } from 'react';
import { createRoot } from 'react-dom/client';
// Memoized Child Component
const Child = memo(({ onButtonClick, user }) => {
console.log("Child re-rendered");
return (
<div className="child">
<h3>Child Component</h3>
<p>User: {user.name} (Age: {user.age})</p>
<button onClick={onButtonClick}>Click Child Button</button>
</div>
);
});
// Parent Component
const Parent = () => {
const [count, setCount] = useState(0);
const [search, setSearch] = useState("");
const [userAge, setUserAge] = useState(30);
// 1. Memoize callback with useCallback (stable reference)
const handleChildClick = useCallback(() => {
alert("Child button clicked!");
}, []); // No dependencies → never re-created
// 2. Memoize user object with useMemo (stable reference)
const user = useMemo(() => ({
name: "John Doe",
age: userAge
}), [userAge]); // Re-create only if userAge changes
// 3. Memoize expensive filter with useMemo
const filteredItems = useMemo(() => {
console.log("Filtering items...");
const largeList = Array.from({ length: 10000 }, (_, i) => `Item ${i}`);
return largeList.filter(item =>
item.toLowerCase().includes(search.toLowerCase())
);
}, [search]); // Re-filter only if search changes
return (
<div className="parent">
<h2>Parent Component</h2>
<p>Count: {count}</p>
<button onClick={() => setCount(prev => prev + 1)}>Increment Count</button>
<p>Search Items: <input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Type to filter..."
/></p>
<p>User Age: {userAge} <button onClick={() => setUserAge(prev => prev + 1)}>Increment Age</button></p>
<Child onButtonClick={handleChildClick} user={user} />
<div className="filtered-items">
<h4>Filtered Items ({filteredItems.length}):</h4>
<ul>{filteredItems.slice(0, 5).map((item, i) => <li key={i}>{item}</li>)}</ul>
{filteredItems.length > 5 && <p>...and {filteredItems.length - 5} more</p>}
</div>
</div>
);
};
// Render to DOM
const root = createRoot(document.getElementById('root'));
root.render(<Parent />);Key Observations#
-
Without
useCallback/useMemo:- The Child component re-renders on every
countincrement (due to newhandleChildClickanduserreferences). - "Filtering items..." logs on every render (even if
searchis unchanged).
- The Child component re-renders on every
-
With
useCallback/useMemo:- Child only re-renders when
userAgechanges (sinceuseris memoized) or ifhandleChildClickdependencies change (none here). - "Filtering items..." only logs when
searchchanges.
- Child only re-renders when
Best Practices#
-
Don’t Prematurely Optimize
Memoization has overhead (storing cached values/functions). Only use these hooks when you observe performance issues (e.g., laggy renders, unnecessary re-renders). -
Keep Dependency Arrays Accurate
Always include all variables used inside the memoized function/calculation in the dependency array. Missing dependencies can lead to stale values (e.g., using outdated state). -
Avoid Side Effects in
useMemo
The function passed touseMemoruns during rendering, so it shouldn’t perform side effects (e.g., API calls). UseuseEffectfor side effects instead. -
Combine with
React.memo
useCallbackanduseMemoare most effective when used withReact.memo(for components) oruseMemo(for object/array props) to prevent unnecessary re-renders. -
Profile First
Use React DevTools’ Profiler tab to identify re-render issues or expensive calculations before optimizing with these hooks.
Common Pitfalls to Avoid#
-
Over-Memoizing Everything
Memoizing trivial functions or cheap calculations wastes memory and can make code harder to read. -
Ignoring Dependency Arrays
Forgetting dependencies (e.g.,useCallback(() => { /* uses count */ }, [])) leads to stale closures, where the memoized function uses outdated state/props. -
Using
useMemofor Side Effects
useMemois for calculations, not side effects. For example:// ❌ Bad: side effect in useMemo useMemo(() => { document.title = `Count: ${count}`; // Side effect! }, [count]); // ✅ Good: use useEffect for side effects useEffect(() => { document.title = `Count: ${count}`; }, [count]); -
Assuming Memoization Guarantees No Re-renders
React.memo(and memoized props) only prevent re-renders if all props are shallowly equal. If other props change, the component will still re-render.
When Not to Use useCallback or useMemo#
- Small Apps/Components with no performance issues.
- Trivial Functions/Calculations (e.g., a simple
() => setCount(1)or[1, 2, 3].map(x => x * 2)). - When Dependencies Change Frequently (memoization won’t help if dependencies update on every render).
Conclusion#
useCallback and useMemo are powerful tools for optimizing React performance, but they’re not silver bullets. useCallback stabilizes function references to prevent unnecessary re-renders of memoized child components, while useMemo caches expensive calculation results to avoid redundant work.
The key is to measure first, then optimize. Use React DevTools to identify bottlenecks, and apply these hooks only when they provide a clear performance benefit. With careful use, they’ll help keep your app fast and responsive as it scales.