โก React.memo and useMemo
A React app that works and a React app that feels fast are two different things. In this lesson you'll learn why React re-renders, when that re-rendering actually costs you, and how the two headline memoization tools โ React.memo and useMemo โ let you skip the work that doesn't need doing. Just as important: how to tell when not to reach for them.
Week 5 · Day 4 (Thursday: Performance Optimization) · Lecture 1
๐ฏ Learning Objectives
By the end of this lesson, you will be able to:
- Explain what triggers a React re-render and why re-renders cascade to children
- Wrap a component in
React.memoto skip re-renders when its props are unchanged - Cache expensive calculations with
useMemoand write correct dependency arrays - Reason about referential equality โ why a fresh object or array defeats memoization
- Combine
React.memoanduseMemoto keep a data-heavy dashboard responsive - Decide when memoizing is worth it, and recognize over-optimization when you see it
Estimated Time: 70 minutes
Practice: Profile a laggy list, then fix it with React.memo and a memoized filter.
In This Lesson
Why Re-renders Happen
Picture a busy restaurant kitchen. A good chef doesn't chop the same onion from scratch every single time a plate goes out โ they prep ingredients once and reuse them. React memoization is the same instinct applied to your UI: remember the result of work you've already done, and reuse it when nothing relevant has changed.
Before you can optimize re-renders, you need to know what causes them. A React function component re-runs (re-renders) when any of these happen:
- Its own state changes (a
useStatesetter fires) - A context value it reads changes
- Its parent re-renders โ even if this component's props are identical
That last one surprises people. By default, when a parent re-renders, React re-renders all of its children, all the way down. Most of the time that's completely fine โ rendering is usually cheap. The problem is the exceptions: a child that does heavy work, or a subtree with hundreds of nodes, re-running on every unrelated keystroke.
even if props never changed]
Memoization gives you two scissors to snip that cascade. React.memo stops the re-render from reaching a component when its props are unchanged. useMemo stops an expensive calculation from re-running inside a component that does re-render. Let's take them one at a time.
๐ "Re-render" is not "repaint"
A re-render means React re-runs your component function and produces a new virtual DOM description. It then diffs that against the previous one and only touches the real DOM where something actually differs. Re-rendering is cheaper than you think โ which is exactly why you should measure before assuming a re-render is your bottleneck.
React.memo: Skipping Re-renders
React.memo is a higher-order component. You wrap a component in it, and React will skip re-rendering that component if its props are the same as last time (compared shallowly). Think of it as a bouncer at the component's door: "Same props as before? You're not coming in โ reuse the previous render."
Basic usage
import { memo, useState } from 'react';
// WITHOUT memo โ re-renders on every parent update, even when `data` is unchanged
function PlainList({ data }) {
console.log('PlainList rendered');
const doubled = data.map(n => n * 2);
return (
<ul>
{doubled.map((n, i) => <li key={i}>{n}</li>)}
</ul>
);
}
// WITH memo โ only re-renders when the `data` prop actually changes
const MemoList = memo(function MemoList({ data }) {
console.log('MemoList rendered');
const doubled = data.map(n => n * 2);
return (
<ul>
{doubled.map((n, i) => <li key={i}>{n}</li>)}
</ul>
);
});
function Parent() {
const [count, setCount] = useState(0);
// `data` is created once and kept stable across renders
const [data] = useState([1, 2, 3, 4, 5]);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
{/* Logs on EVERY click โ count changed, so Parent re-renders it */}
<PlainList data={data} />
{/* Logs only ONCE โ `data` never changes, so memo skips it */}
<MemoList data={data} />
</div>
);
}
Click the button and watch the console. PlainList logs every click; MemoList logs once and then stays quiet, because data is referentially the same array each time. That last phrase โ "referentially the same" โ is the whole game, and we'll return to it in its own section.
๐ก Note on React.memo vs memo
Modern React lets you import memo directly: import { memo } from 'react'. You'll also see React.memo(...) in code that imports the default React object โ they're the same function. We'll use the named import going forward.
How the comparison works
By default memo does a shallow comparison of props: for each prop, it checks Object.is(prevProp, nextProp). Primitives (numbers, strings, booleans) compare by value, so they "just work." Objects, arrays, and functions compare by reference โ two identical-looking objects are still different if they're two separate objects in memory.
memo shallow-compares each prop. Equal props short-circuit the render; any difference lets the render through.A custom comparison function
You can pass a second argument to memo that decides equality yourself. It returns true to skip the render (props considered equal) and false to re-render. This is the opposite polarity of an array's sort comparator, so read it carefully.
const UserCard = memo(
function UserCard({ user }) {
console.log('UserCard rendered for:', user.name);
return (
<div className="user-card">
<h3>{user.name}</h3>
<p>Email: {user.email}</p>
<p>Last active: {user.lastActive}</p>
</div>
);
},
// Return true = "equal, skip". Only re-render when name or email changes;
// deliberately ignore lastActive so a heartbeat update doesn't re-render.
(prev, next) =>
prev.user.name === next.user.name &&
prev.user.email === next.user.email
);
โ ๏ธ Custom comparators are a sharp tool
A hand-written comparator is easy to get subtly wrong โ forget a field and the UI silently goes stale. Reserve it for real, measured cases (like ignoring a noisy field). Most of the time, the default shallow comparison plus stable props is the better answer.
When React.memo earns its keep
- The component is expensive to render (large list, chart, complex tree).
- It re-renders often because its parent updates frequently for unrelated reasons.
- It's usually given the same props across those re-renders.
If a component is trivial โ a button, a label โ wrapping it in memo can cost more (the comparison itself) than it saves. Don't sprinkle it everywhere.
useMemo: Caching Calculations
Where React.memo guards a whole component, useMemo guards a single value computed inside a component. It runs a function and remembers its result, only recomputing when one of its dependencies changes. It's a cache with a very precise invalidation rule.
Basic usage
import { useMemo } from 'react';
// WITHOUT useMemo โ filter runs on EVERY render, even unrelated ones
function FilteredList({ items, query }) {
console.log('Filtering (every render)...');
const filtered = items.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
);
return <List items={filtered} />;
}
// WITH useMemo โ filter only re-runs when items OR query changes
function OptimizedFilteredList({ items, query }) {
const filtered = useMemo(() => {
console.log('Filtering (only when deps change)...');
return items.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
);
}, [items, query]); // โ dependency array
return <List items={filtered} />;
}
The dependency array is the contract: "recompute only if one of these changed." Get it right and the cache is correct and fast. Get it wrong โ leave out a value the calculation reads โ and you'll serve a stale result.
โ The ESLint rule that saves you
Install eslint-plugin-react-hooks and enable react-hooks/exhaustive-deps. It flags missing dependencies in useMemo, useCallback, and useEffect โ the single most common source of memoization bugs. Treat its warnings as errors until you understand exactly why you're overriding one.
Genuinely expensive work
useMemo's sweet spot is a calculation that's actually costly โ statistics over a large array, parsing, building derived data structures. Here everything descends from data, so we only pay when data changes:
function DataAnalytics({ data, thresholds }) {
// Expensive: statistics over a potentially huge array
const stats = useMemo(() => {
console.log('Calculating statistics...');
const mean = data.reduce((sum, v) => sum + v, 0) / data.length;
const variance =
data.reduce((sum, v) => sum + (v - mean) ** 2, 0) / data.length;
return { mean, stdDev: Math.sqrt(variance) };
}, [data]);
// Depends on `stats` AND `thresholds` โ recompute if either changes
const rows = useMemo(() => {
console.log('Transforming rows...');
return data.map(value => ({
value,
zScore: (value - stats.mean) / stats.stdDev,
band: value > thresholds.high ? 'high'
: value < thresholds.low ? 'low' : 'mid',
}));
}, [data, stats, thresholds]);
return (
<div>
<p>Mean {stats.mean.toFixed(2)} ยท Std dev {stats.stdDev.toFixed(2)}</p>
<DataTable rows={rows} />
</div>
);
}
Memoizing objects and arrays for stable references
useMemo has a second job beyond saving CPU: producing a stable reference for an object or array you pass to a memoized child or a hook dependency. Even if building the object is cheap, giving the child the same object each render is what lets React.memo do its work.
function SearchPanel({ defaultFilters }) {
const [term, setTerm] = useState('');
const [order, setOrder] = useState('asc');
// โ A brand-new object every render โ any memoized child sees "new props"
const badConfig = { term, order, ...defaultFilters };
// โ
Same object identity until term/order/defaultFilters actually change
const config = useMemo(
() => ({ term, order, ...defaultFilters }),
[term, order, defaultFilters]
);
// This list never changes โ build it once with an empty dep array
const sortOptions = useMemo(() => [
{ value: 'asc', label: 'Ascending' },
{ value: 'desc', label: 'Descending' },
], []);
return (
<div>
<SortSelector options={sortOptions} value={order} onChange={setOrder} />
<FilteredResults config={config} />
</div>
);
}
This "stable reference" role is the bridge to the next section โ and to the next lesson, where useCallback does exactly this for functions.
Referential Equality: The Idea That Ties It Together
Here's the trap that catches everyone. You wrap a child in React.memo, expecting it to stop re-rendering โ but it re-renders anyway. Why? Because you're handing it a fresh object, array, or function on every render, and those compare by reference.
function Parent() {
const [count, setCount] = useState(0);
// ๐ด A new object literal every single render
const config = { color: 'blue', size: 'large' };
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
{/* MemoChild re-renders every click โ `config` is never "equal" */}
<MemoChild config={config} />
</div>
);
}
Every time Parent renders, { color: 'blue', size: 'large' } creates a new object. To React's shallow comparison, prevConfig !== nextConfig, so the memoized child re-renders. The fix is to make the reference stable with useMemo:
function ParentFixed() {
const [count, setCount] = useState(0);
// โ
Built once, reused every render โ reference stays equal
const config = useMemo(() => ({ color: 'blue', size: 'large' }), []);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<MemoChild config={config} /> {/* now truly skips re-renders */}
</div>
);
}
๐ก The mental model
React.memo is only as good as the stability of the props you feed it. Memoizing a child is a two-part contract: wrap the child and keep its object/array/function props referentially stable. Miss the second half and the first half does nothing.
The same reasoning explains why passing an inline arrow function โ onClick={() => doThing()} โ to a memoized child defeats the memo: a new function is created each render. That specific problem is what useCallback exists to solve, and it's the whole topic of the next lesson.
Combining React.memo and useMemo
Real optimization usually layers them. The parent uses useMemo to compute derived data once and to keep props stable; the children are wrapped in React.memo so they only re-render when their specific slice of data changes.
import { memo, useMemo, useState } from 'react';
function DataDashboard({ rawData, filters }) {
const [sortConfig, setSortConfig] = useState({ key: 'name', order: 'asc' });
// Expensive pipeline โ recompute only when its real inputs change
const processedData = useMemo(() => {
console.log('Processing data...');
return rawData
.filter(item => applyFilters(item, filters))
.map(enrichData)
.sort((a, b) => sortData(a, b, sortConfig));
}, [rawData, filters, sortConfig]);
// Summary stats derive from the processed data
const stats = useMemo(() => ({
total: processedData.length,
average: processedData.reduce((s, x) => s + x.value, 0) /
(processedData.length || 1),
}), [processedData]);
return (
<div>
<Statistics stats={stats} />
<DataTable data={processedData} sortConfig={sortConfig} />
</div>
);
}
// Child re-renders only when `stats` changes โ not on every parent render
const Statistics = memo(function Statistics({ stats }) {
console.log('Statistics rendered');
return (
<div className="statistics">
<div>Total: {stats.total}</div>
<div>Average: {stats.average.toFixed(2)}</div>
</div>
);
});
const DataTable = memo(function DataTable({ data, sortConfig }) {
console.log('DataTable rendered');
return (
<table>
<tbody>
{data.map(item => (
<tr key={item.id}><td>{item.name}</td><td>{item.value.toFixed(2)}</td></tr>
))}
</tbody>
</table>
);
});
Notice the shape: useMemo both saves the expensive processing and hands each memoized child a stable prop. The two hooks aren't rivals โ they're partners covering different halves of the same problem.
๐ A word on the React Compiler
Newer React tooling (the React Compiler) can insert a lot of this memoization for you automatically, so hand-written memo/useMemo may shrink over time. It's still essential to understand the underlying model: the compiler optimizes the same re-render and referential-equality mechanics you're learning here, and you'll still read and maintain plenty of code that memoizes by hand.
Measure First: Don't Optimize Blind
The cardinal rule of performance work: measure before you optimize. Memoization adds code, comparison cost, and memory. If a component isn't actually slow, adding memo and useMemo makes your code harder to read for zero benefit โ sometimes for negative benefit.
React DevTools Profiler
The React DevTools browser extension has a Profiler tab. Record an interaction, and it shows you a flame graph of what rendered, how long each component took, and โ crucially โ why each one rendered. Start there. Optimize the components that actually show up hot.
A tiny "why did this render?" hook
For quick console-level insight, this hook logs which props changed between renders โ a fast way to catch a referential-equality leak:
import { useRef, useEffect } from 'react';
function useWhyDidYouUpdate(name, props) {
const previous = useRef();
useEffect(() => {
if (previous.current) {
const changed = {};
for (const key of Object.keys({ ...previous.current, ...props })) {
if (previous.current[key] !== props[key]) {
changed[key] = { from: previous.current[key], to: props[key] };
}
}
if (Object.keys(changed).length) {
console.log('[why-did-you-update]', name, changed);
}
}
previous.current = props;
});
}
// Usage: drop it at the top of a component you suspect is re-rendering too much
// useWhyDidYouUpdate('DataTable', { data, sortConfig });
If it reports that config or onClick "changed" on every render even though they look identical โ congratulations, you've found a referential-equality problem, and now you know exactly which prop to stabilize.
Practice & Quiz
๐๏ธ Exercise 1: Stop the leak
Goal: This memoized child re-renders on every click even though settings looks unchanged. Find out why and fix it โ the child should render only once.
const Panel = memo(function Panel({ settings }) {
console.log('Panel rendered');
return <div>{settings.mode}</div>;
});
function App() {
const [count, setCount] = useState(0);
const settings = { mode: 'dark' }; // ๐ something's off here
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<Panel settings={settings} />
</div>
);
}
๐ก Hint
settings is a fresh object literal on every render, so React.memo's shallow comparison always sees a new reference. Give it a stable identity.
โ Solution
function App() {
const [count, setCount] = useState(0);
// Stable reference โ built once, reused every render
const settings = useMemo(() => ({ mode: 'dark' }), []);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<Panel settings={settings} />
</div>
);
}
// Panel now logs exactly once.
๐๏ธ Exercise 2: Memoize an expensive derived value
Goal: A component filters a large list on every render, including when an unrelated theme state toggles. Wrap the filter in useMemo with a correct dependency array.
โ Solution
function ProductList({ products, query }) {
const [theme, setTheme] = useState('light'); // unrelated to filtering
const visible = useMemo(() => {
return products.filter(p =>
p.name.toLowerCase().includes(query.toLowerCase())
);
}, [products, query]); // theme is NOT a dependency โ filtering ignores it
return (
<div className={theme}>
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Toggle theme
</button>
<ul>{visible.map(p => <li key={p.id}>{p.name}</li>)}</ul>
</div>
);
}
// Toggling the theme no longer re-runs the filter.
๐ฏ Quick Quiz
Question 1: What does React.memo compare by default to decide whether to skip a re-render?
Question 2: You wrapped a child in React.memo but it still re-renders every time the parent updates. What's the most likely cause?
Question 3: When is useMemo genuinely worth adding?
Best Practices & Pitfalls
โ Do
- Measure first with the React DevTools Profiler โ optimize the components that are actually hot
- Keep dependency arrays complete and honest; let
exhaustive-depsguide you - Use
useMemoto keep object/array props referentially stable for memoized children - Reach for
React.memoon components that are expensive and often re-rendered with the same props - Move truly constant values outside the component so they never need to be dependencies
โ Don't
- Wrap every component in
memoโ the comparison isn't free, and it clutters the code - Memoize trivial math like
count * 2; the hook costs more than the calculation - Leave a value out of the dependency array to "make it stop recomputing" โ that ships a stale bug
- Assume a re-render is the bottleneck without profiling; rendering is often cheap
- Write a custom comparator unless you've measured a specific need โ it's easy to make stale
โ ๏ธ The stale-dependency trap
// ๐ด WRONG: `query` is read but missing from deps โ stale results
const filtered = useMemo(() => {
return items.filter(i => i.includes(query));
}, [items]); // should be [items, query]
// โ
RIGHT
const filtered = useMemo(() => {
return items.filter(i => i.includes(query));
}, [items, query]);
If a value appears inside the memo function, it belongs in the dependency array. Removing it doesn't fix a performance problem โ it hides a correctness one.
Summary
๐ Key Takeaways
- A component re-renders on state, context, or parent changes โ parents re-render children by default
React.memoskips a component's re-render when its props are shallowly equaluseMemocaches an expensive value and recomputes only when its dependencies change- Referential equality is the linchpin: a fresh object/array/function prop defeats
memo - Use
useMemoto keep props stable, then letReact.memodo its job - Measure first โ don't pay memoization's cost where there's no benefit
๐ Additional Resources
- react.dev โ
memo - react.dev โ
useMemo - react.dev โ Render and Commit (how re-rendering works)
- react.dev โ React Developer Tools & the Profiler
๐ What's Next?
You've seen that inline functions break memoization because they get a new reference every render. The next lesson introduces the hook built precisely for that problem: useCallback โ memoizing function references so your React.memo children finally stay put.
โก Great pace!
You can now reason about why React re-renders and cut the work that doesn't need repeating โ measuring before you reach for the scissors.