React.memo, useMemo, useCallback: What, When, Why
Memoization sounds like one of those things you should do. Like flossing. Or using useCallback
. But if you've ever spent an hour wrapping every function and component in memo()
only to get zero performance gain—or worse, made things slower—you're not alone. I’ve done that dance too. Here's how memoization in React actually works, when it matters, and why the common advice often leads you astray.
React.memo Isn’t Magic
Let’s kill the myth upfront.
React.memo
does not make your app faster by default. What it really does is prevent a component from re-rendering unless its props change—using a shallow comparison.
const MyComponent = React.memo(function MyComponent(props) {
return <div>{props.title}</div>;
});
That’s it. No deep magic. No virtual DOM time travel.
And here’s the catch: if your component renders fast anyway, React.memo
might actually hurt performance. React still has to compare the props every single time. If you’re wrapping every Card
, Button
, or ListItem
just because you can, you’re adding more overhead than you’re saving.
Memoing dumb components is like wrapping instant noodles in bubble wrap. Doesn’t help. Slows you down.
I once memoized a static footer and felt clever—until I opened the profiler and saw it added milliseconds for no gain. Lesson learned.
useMemo vs useCallback Explained
They look similar. They’re not.
useMemo memoizes values. useCallback memoizes functions.
If you’re thinking, “Cool. So I’ll just use both everywhere,” pump the brakes.
Here's how `useMemo` works:
const expensiveValue = useMemo(() => computeExpensiveStuff(data), [data]);
React only recalculates computeExpensiveStuff
if data
changes. Great for heavy operations—like filtering thousands of items or sorting complex data structures.
And `useCallback`?
const handleClick = useCallback(() => {
doSomething();
}, [doSomething]);
This ensures the handleClick
function keeps the same identity across renders—unless its dependencies change.
Why does this matter?
Let’s say you’re passing handleClick
to a memoized child. If the function reference changes on every render, your memoized component re-renders anyway. Defeats the whole purpose.
But—don’t memoize every function "just in case." That’s like buying insurance for your toaster. Keep things simple unless you need stability.
Building a Slow Component and Fixing It
Let me walk you through something I botched once. Here’s a component that looked innocent:
function ProductList({ products, onClick }) {
console.log('Rendered ProductList');
return products.map(product => (
<div key={product.id} onClick={() => onClick(product)}>
{product.name}
</div>
));
}
Every time the parent rendered, this re-rendered too—even if products
and onClick
didn’t change.
Turns out, I was passing an inline `onClick` function from the parent. Which means a new function was created every render. So ProductList
thought its props changed. Boom. Re-render.
Fixed with `React.memo` + `useCallback`:
const ProductList = React.memo(({ products, onClick }) => {
return products.map(product => (
<div key={product.id} onClick={() => onClick(product)}>
{product.name}
</div>
));
});
And in the parent:
const handleProductClick = useCallback((product) => {
// do something
}, []);
<ProductList products={products} onClick={handleProductClick} />;
Now ProductList
only re-renders when products
or handleProductClick
actually change. That’s the power of stable props + memoization working together.
Pitfalls of Premature Memoization
Here’s the thing nobody warns you about: memoization can backfire.
I’ve seen devs (me included) wrap 80% of their components in React.memo
, throw useMemo
and useCallback
everywhere, and feel smug about it.
But unless you measure the performance before and after, you’re just guessing. Worse—React has to spend extra CPU time checking props or dependencies every render. If your component was rendering in 2ms, and now takes 4ms to maybe avoid re-rendering? You’ve just made it slower.
Memoization is not free. Use it when:
You’ve got real render slowness You’re passing props to memoized children * Your callback functions cause unnecessary updates
Otherwise? Leave it out. Over-optimization is a bug factory.
Practical Patterns and Naming Conventions
Memoization patterns don’t need to be fancy. Just clear.
🔹 Name functions and values like they matter:
const useSortedItems = useMemo(() => sort(items), [items]);
const handleFormSubmit = useCallback(() => submit(form), [form]);
🔹 Memoize expensive stuff—not everything:
const sortedData = useMemo(() => heavySort(data), [data]);
Don’t use useMemo(() => x, [])
just to “cache” a value that doesn’t need caching. That’s not optimization—it’s superstition.
🔹 Profile first. Always.
Don’t assume. Open the React DevTools Profiler and look at render times and re-render causes. This one tool will save you hours of pointless memoization. (If you’re curious about digging into that, React’s official docs on the Profiler are a solid read.)
And when you’re ready to turn these patterns into real performance wins, check out the next lesson: **Avoiding Unnecessary Renders (Without Overthinking It)** — because that’s where it all starts to pay off.
Ready for the next one? I can continue with Avoiding Unnecessary Renders next if you’d like.