useMemo and useCallback, honestly
I've watched teams sprinkle useMemo and useCallback across their codebase like they're free performance pills. They're not. Every memoization hook carries a cost—it stores data, runs comparisons, and takes up mental overhead. The real question isn't "should I memoize this?" but "does my app actually need it right now?" Let me walk through when these tools genuinely help and when they're just adding noise.
They cost something — Every memo stores values and compares dependencies on each render
This is the part nobody talks about. useMemo and useCallback don't magically make your app faster. They add overhead: memory for storing cached values, CPU cycles for dependency array comparisons, and browser time to run the comparison logic itself.
Here's what happens on every render:
- React runs your component function.
- If you've wrapped something in useMemo or useCallback, React compares the old dependency array to the new one.
- If dependencies changed, it recomputes. If not, it returns the cached version.
- This comparison and caching logic itself has a cost.
For most calculations—string formatting, object transformations, simple array operations—the memoization overhead is larger than the computation itself. Let me show you:
// Bad: useMemo for cheap work
const UserCard = ({ user }) => {
const fullName = useMemo(() => {
return `${user.firstName} ${user.lastName}`;
}, [user.firstName, user.lastName]);
return <div>{fullName}</div>;
};
// Good: Just compute it
const UserCard = ({ user }) => {
const fullName = `${user.firstName} ${user.lastName}`;
return <div>{fullName}</div>;
};
The string concatenation takes microseconds. Comparing dependencies, allocating memory for the cached value, and checking on every render? That's measurable overhead in the wrong direction. The browser spends more energy on the memoization than on the actual work.
You pay in:
- Memory: useMemo allocates heap space for cached values.
- Comparison time: Dependency arrays are checked every render—primitive comparison for numbers/strings, referential equality for objects.
- Cognitive load: You have to maintain that dependency array. Get it wrong, and you've cached stale data.
Use memoization when the benefit of skipping expensive computation outweighs these costs. Not before.
When useMemo helps — Expensive calculations, or stable objects passed to memoized children
useMemo earns its place in two scenarios: truly expensive computations and creating stable object references for memoized children.
Scenario 1: Expensive calculations
If you're doing heavy lifting—sorting a list of 10,000 items, running complex statistics, parsing large data structures—useMemo can prevent needless recalculation:
const AnalyticsDashboard = ({ transactions }) => {
// This sorting is genuinely expensive with large datasets
const sortedTransactions = useMemo(() => {
console.log('Sorting transactions...');
return [...transactions].sort((a, b) => b.amount - a.amount);
}, [transactions]);
return (
<div>
{sortedTransactions.map(t => (
<TransactionRow key={t.id} transaction={t} />
))}
</div>
);
};
Here, if the transactions array hasn't changed, we skip the sort entirely. That's a real win because sorting scales with O(n log n) complexity. With 10,000 items, that's thousands of comparisons avoided.
Scenario 2: Stable objects for React.memo children
This is where useMemo and memoization interact. If you pass an object or array as a prop to a component wrapped in React.memo, and that object is created fresh on every render, React.memo can't help you—the prop reference changes even if the data doesn't.
// Child wrapped in React.memo
const UserProfile = React.memo(({ user }) => {
console.log('UserProfile rendered');
return <div>{user.name}</div>;
});
// Without useMemo: re-renders every time
const ParentBad = () => {
return <UserProfile user={{ name: 'Alice', id: 1 }} />;
};
// With useMemo: renders only when user data changes
const ParentGood = () => {
const user = useMemo(() => ({ name: 'Alice', id: 1 }), []);
return <UserProfile user={user} />;
};
In ParentBad, even though the user object has the same shape, it's a new object reference on every render. React.memo sees user !== oldUser and re-renders the child. In ParentGood, useMemo keeps the object stable, so React.memo can skip the re-render.
Without useMemo, your React.memo memoization is broken. With it, the child actually stays memoized.
When useCallback helps — Functions passed to React.memo children or used as effect dependencies
useCallback is the function equivalent of useMemo. It keeps a function reference stable across renders. It matters in two places.
Scenario 1: Callbacks passed to memoized children
If a child component is wrapped in React.memo and you pass a callback prop, that callback must be stable. Creating a new function on every render defeats the memoization:
const DeleteButton = React.memo(({ onDelete }) => {
console.log('DeleteButton rendered');
return <button onClick={onDelete}>Delete</button>;
});
// Without useCallback: child re-renders every render
const TodoItemBad = ({ id }) => {
return (
<DeleteButton onDelete={() => console.log('Deleting', id)} />
);
};
// With useCallback: child only re-renders if id changes
const TodoItemGood = ({ id }) => {
const handleDelete = useCallback(() => {
console.log('Deleting', id);
}, [id]);
return <DeleteButton onDelete={handleDelete} />;
};
In TodoItemBad, the arrow function is recreated on every render. React.memo sees a new function reference and re-renders. In TodoItemGood, useCallback keeps the function stable until id changes, letting React.memo actually work.
Scenario 2: Effect dependencies
This is subtle. If you use a function inside useEffect and don't stabilize it with useCallback, you'll trigger infinite effect loops:
const FetchUserData = ({ userId }) => {
// Without useCallback: effect runs on every render
const fetchData = () => {
fetch(`/api/users/${userId}`).then(r => r.json());
};
useEffect(() => {
fetchData();
}, [fetchData]); // fetchData is new every render!
};
// With useCallback: effect runs only when userId changes
const FetchUserDataFixed = ({ userId }) => {
const fetchData = useCallback(() => {
fetch(`/api/users/${userId}`).then(r => r.json());
}, [userId]);
useEffect(() => {
fetchData();
}, [fetchData]);
};
Without useCallback, fetchData is redefined on every render, so [fetchData] changes, triggering the effect again, which redefines fetchData... infinite loop. useCallback stabilizes it.
The React Compiler — React Compiler 1.0 memoizes automatically. Fewer manual hooks going forward
Here's the exciting part: React is working on a compiler that automatically memoizes for you. The React Compiler (released as experimental, heading to stable) analyzes your component code and injects memoization where it helps, without you writing a single useMemo or useCallback.
From the React team's perspective, manual memoization is a primitive escape hatch. They want the compiler to do it right, at scale, with dependency tracking that's hard for humans to get wrong.
When you enable the React Compiler in your build:
// You write this—just normal code
const UserCard = ({ user, onDelete }) => {
const fullName = `${user.firstName} ${user.lastName}`;
const handleDelete = () => {
onDelete(user.id);
};
return (
<div>
<h3>{fullName}</h3>
<button onClick={handleDelete}>Delete</button>
</div>
);
};
// Compiler transforms it (conceptually) into this
const UserCard = ({ user, onDelete }) => {
const fullName = useMemo(
() => `${user.firstName} ${user.lastName}`,
[user.firstName, user.lastName]
);
const handleDelete = useCallback(() => {
onDelete(user.id);
}, [user.id, onDelete]);
return (
<div>
<h3>{fullName}</h3>
<button onClick={handleDelete}>Delete</button>
</div>
);
};
The compiler understands value semantics better than humans do. It won't over-memoize cheap work. It gets dependency arrays right. It's the endgame for performance in React.
For now, if you're using React 18 without the compiler, you need to memoize manually and intentionally. But going forward—especially as Next.js integrates the compiler more deeply—you'll write less of this boilerplate.
Common mistakes
- Memoizing everything "just in case": This adds overhead without benefit. Memoize what you've measured to be slow or what genuinely needs stable references.
- Wrong dependency arrays: If you omit a dependency, you cache stale data. If you include too many, memoization breaks because the cache invalidates on every render anyway. This is why the compiler is better—it can't miss dependencies.
- Forgetting React.memo on the child: If you use useCallback to pass stable props but don't wrap the child in React.memo, the child still re-renders. useCallback alone doesn't prevent re-renders; it just creates stable references for React.memo to rely on.
- Memoizing in the wrong place: Wrapping expensive work in useMemo is good. Wrapping every filter or map is not. Understand what's actually expensive in your app—use your profiler.
- Using useCallback without React.memo: If the component using the callback doesn't need to stay memoized, useCallback is wasted effort. It only helps when you're optimizing re-renders of children or preventing effect loops.
Key takeaways
- useMemo and useCallback have a cost. Memory, comparison logic, dependency array maintenance. Use them only when the benefit justifies the overhead.
- useMemo helps for expensive calculations (sorting large arrays, complex computations) and for creating stable object references that memoized children depend on.
- useCallback helps for passing stable function references to React.memo children and for preventing infinite useEffect loops when functions are effect dependencies.
- Always pair useCallback or stable object references with React.memo on the child component. Otherwise, the child re-renders anyway and you've added overhead for nothing.
- Profile first. Don't memoize by habit. React DevTools Profiler shows you where time is actually spent.
- The React Compiler is coming and will automate memoization. Write normal code; let the compiler handle the optimization.
- In production codebases, memoization matters most at scale: large lists, expensive calculations, deeply nested component trees with frequent re-renders.






Comments
Loading comments…