When to Use React.memo, useMemo and useCallback
Decide when React memoization helps using a runnable list example, a before-and-after profiling exercise, and common reasons optimizations fail.
On this page
When should you use memoization in React?
Use memoization when a measured interaction repeats work that can be skipped because its inputs have not changed. First identify the slow calculation or component, then stabilize only the values and callbacks that let it avoid that work. Measure the same interaction again before keeping the optimization.
If you need the API definitions or a side-by-side comparison, start with useMemo vs useCallback. This lesson focuses on making and testing an optimization decision.
Choose the optimization that matches the work
| Observed problem | Candidate change | Condition for helping |
|---|---|---|
| An expensive calculation repeats on unrelated updates | useMemo |
Its dependencies usually stay the same during those updates |
| An expensive child renders when its parent updates | React.memo |
The child's props remain equal; its own state or context can still update |
| A new callback prevents a memoized child from skipping | useCallback |
Every other prop passed to the child is also stable |
| Typing in a small form updates a large unrelated tree | Move the form's state closer to the input | The large tree no longer depends on that state |
Changing a callback alone does not make an ordinary child skip rendering. Conversely, memoization is not always needed: moving state, removing an unnecessary Effect, or rendering fewer rows may solve the underlying problem more directly.
A reproducible before-and-after exercise
Paste this into a client-rendered React app as App.tsx. It works without React Compiler; disable compiler transformation for this exercise if your project enables it so that you can isolate manual memoization.
The list intentionally recalculates a score for every item. This makes repeated work easy to inspect, but it is a teaching workload rather than a benchmark for your product.
'use client';
import { memo, useCallback, useMemo, useState } from 'react';
type Question = { id: number; title: string };
type ListProps = {
questions: Question[];
onSelect: (id: number) => void;
};
const QUESTIONS: Question[] = Array.from({ length: 600 }, (_, id) => ({
id,
title: `Interview question ${id + 1}`,
}));
function practiceScore(id: number) {
let score = 0;
for (let step = 0; step < 2000; step += 1) {
score += Math.sqrt(id + step);
}
return Math.round(score);
}
function QuestionList({ questions, onSelect }: ListProps) {
return (
<ul>
{questions.map((question) => (
<li key={question.id}>
<button onClick={() => onSelect(question.id)}>
{question.title}: {practiceScore(question.id)}
</button>
</li>
))}
</ul>
);
}
const MemoQuestionList = memo(QuestionList);
export default function App() {
const [query, setQuery] = useState('');
const [count, setCount] = useState(0);
const [selected, setSelected] = useState<number | null>(null);
// Baseline: a new array and callback on every parent render.
const visible = QUESTIONS.filter((question) =>
question.title.toLowerCase().includes(query.toLowerCase())
);
const onSelect = (id: number) => setSelected(id);
return (
<main>
<label>
Filter questions
<input value={query} onChange={(event) => setQuery(event.target.value)} />
</label>
<button onClick={() => setCount((value) => value + 1)}>
Unrelated counter: {count}
</button>
<p>Selected question: {selected ?? 'none'}</p>
<QuestionList questions={visible} onSelect={onSelect} />
</main>
);
}The memo, useMemo, and useCallback imports and MemoQuestionList declaration are reserved for the optimized version below. Your linter may flag them as unused in the baseline.
Apply one change at a time
- Change
<QuestionList ... />to<MemoQuestionList ... />. The list still renders on counter updates: both props get new references. - Replace the
visibleandonSelectdeclarations with the following:
const visible = useMemo(
() => QUESTIONS.filter((question) =>
question.title.toLowerCase().includes(query.toLowerCase())
),
[query]
);
const onSelect = useCallback((id: number) => setSelected(id), []);Now the memoized child can skip list work when the unrelated counter changes. Filtering still changes visible, so the list should render when the query changes. The state setter is stable and the callback does not read selected, so no changing state dependency is needed.
Here useMemo is useful primarily because it preserves the array passed to a memoized child, not because filtering 600 short strings is necessarily expensive.
Record evidence, not assumed speedups
Use React DevTools Profiler to record five counter clicks after the initial render. Repeat with the same query, device, and build after each change. Inspect whether the list renders and compare update durations. Development Strict Mode can repeat render work; do not interpret its logs as production timing. To assess user-visible latency, also compare production builds in the browser Performance panel. React component profiling in production requires a profiling-enabled build.
| Version | Expected behavior on counter update | Your measured update duration |
|---|---|---|
| Ordinary list | List renders | Record locally |
| Memoized list, new props | List still renders | Record locally |
| Memoized list, stable props | List can skip rendering | Record locally |
| Optimized list, query changes | List renders with matching questions | Record separately |
These are expected behaviors, not fabricated timing results. If the list is already cheap on your target device, the extra memoization may not justify its complexity. For more on interpreting a recording, see render vs commit and the Profiler.
Why did memoization fail?
- One prop still changes: an inline object, freshly filtered array, or callback can defeat the memoized child's prop comparison.
- Dependencies change every time: an object created in the component body is a new reference on each render. Simplify the dependency or create it inside the calculation.
- The child updates itself: local state and consumed context can trigger rendering even with stable props.
- The callback has stale data: omitting a dependency may hide an update while introducing a correctness bug. Fix the dependency list rather than silencing the linter.
- The cost is elsewhere: memoization does not fix network delays or a huge initial DOM. Inspect the interaction before choosing the tool.
Avoid custom equality functions until you can explain their correctness and show that comparison is cheaper than rendering. Ignoring a changed function prop can leave a child calling a callback that closes over old state.
What changes with React Compiler?
When React Compiler is installed and enabled for the code in question, it can supply memoization automatically. Upgrading React alone does not mean the compiler is active. Profile the actual build configuration before adding manual caches, and keep calculation functions pure regardless of who supplies the optimization. See the compiler installation guide.
Practice: explain which update should render
Run the example and predict what happens after: (1) clicking the counter, (2) entering a different query, and (3) selecting a question. Then verify your predictions in the Profiler.
Answer and explanation
In the optimized version, the counter and selection changes leave the list props stable, so the list can skip rendering. A query change creates a new filtered array and updates the list. If a selected-row highlight becomes a new prop, selection changes may legitimately require the list to render again.
Explain which work was avoided and which prop made that possible. Continue with the React performance checklist, or revisit the hook comparison if the dependency rules are unclear.
Looking for structured, offline interview prep?
Free online lessons • Portable Amazon Kindle / Paperback
Frontend Interview Guide (2025 Edition)
By Rajesh Dhiman
Comprehensive preparation covering React internals, Web Performance, CSS architecture, and system design questions.
JavaScript Interview Guide
By Rajesh Dhiman
Deep dive into the Event Loop, microtask draining, closures, prototypal inheritance, and asynchronous execution patterns.
Note: All lessons on Frontend Interview remain completely free to read online. As an Amazon Associate, we may earn from qualifying purchases.
Written by Rajesh Dhiman
Authored by Rajesh Dhiman and engineered at Eunix Tech. Focused on deep-dive frontend engineering, JavaScript runtime internals, React architecture, and web performance.