Chapter 18 of 20 · React for Beginners — Complete Free Tutorial
Memoizing with useMemo and useCallback
By default, changing state re-renders a component and everything below it. In most apps that is cheap and correct — React is fast enough. But occasionally a render does real work: an expensive sort, a heavy computation, or a function passed to a memoised child that resets its cache every render. The React for Beginners series reaches this chapter with a firm rule first: measure before you memoise. This chapter explains re-renders, then useMemo, useCallback and when each genuinely pays off.
What triggers a re-render
Knowing when a component runs again is half the battle:
| Triggers a re-render | Does not |
|---|---|
| Its own state changes | Parent re-renders with the same props (unless memoised) |
| A parent re-renders and passes new props | Context value it does not consume |
| Its context value changes | DOM events on other elements |
Re-rendering is not the same as re-drawing the DOM: React recomputes the virtual tree and diffs it. The cost of an extra render is usually the component's own JavaScript — which is exactly what memoisation targets.
useMemo: cache a computed value
useMemo remembers the result of a calculation across renders and returns the cached value until one of its dependencies changes. Use it when the computation is genuinely expensive:
function ProductList({ products, category }) {
const visible = useMemo(() => {
console.log('filtering…');
return products
.filter((p) => p.category === category)
.sort((a, b) => a.price - b.price);
}, [products, category]);
return <ul>{visible.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}
Expected output: typing into an unrelated search box higher in the tree re-renders this list, but "filtering…" logs only when products or category actually changes. The calculation runs on first render and on dependency change — every other render reuses the cached array.
useCallback: cache a function
A plain function is re-created on every render. That is harmless by itself, but it matters when the reference is a dependency — of an effect, of another memo, or of a child wrapped in React.memo. useCallback returns the same function object until its deps change:
const [query, setQuery] = useState('');
// new function identity every render → memoised child re-renders anyway
const handleSearch = (value) => setQuery(value);
// cached identity → memoised child can skip re-rendering
const handleSearch = useCallback((value) => setQuery(value), []);
Expected output: with React.memo on the child, the cached version keeps its reference stable across query keystrokes — the expensive list inside the child does not re-render unless its own props change.
The three tools compared
| Tool | Caches | Invalidated when |
|---|---|---|
useMemo | A computed value | Any dependency changes |
useCallback | A function (its reference) | Any dependency changes |
React.memo | A component's render result | Its props change (shallow compare) |
They are layers of the same idea. React.memo on a child is pointless if the parent passes fresh function references every render — that is the pairing that makes useCallback meaningful. useMemo also serves a second purpose: keeping a value referentially stable when it appears in dependency arrays.
When not to memoise
- Trivial work — filtering fifty items costs microseconds; the memo machinery can outweigh the savings.
- Before profiling — use the React DevTools Profiler, record a real interaction, and let the numbers name the hot component.
- Every value "just in case" — memoisation adds dependency bookkeeping and stale-value risk; it is a tool, not a default.
- To fix a logic bug — an infinite loop or a stale closure is a dependency or state-ownership problem; memoising hides it.
A reasonable order of attack: simplify first (move work out of render, split components), profile, then add memoisation where the profiler points.
Correct dependencies
Both hooks take a dependency array with the same rules as useEffect: list everything you read. Omitting a dependency yields a cached value built from stale inputs — the classic "memo returns old data" bug. Let eslint-plugin-react-hooks enforce it.
// ❌ reads taxRate but hides it from React
const total = useMemo(() => items.reduce((s, i) => s + i.price, 0), [items]);
// ✅ every input listed
const total = useMemo(
() => items.reduce((s, i) => s + i.price * (1 + taxRate), 0),
[items, taxRate],
);
Expected output: the second version recomputes when either the items or the tax rate changes; the first silently shows an outdated total after a tax update.
Common mistakes
- Memoising everything — overhead and complexity with no measured benefit; profile first.
- Missing dependencies — cached values go stale; list every referenced value.
- useCallback without a memoised consumer — wrapping handlers no effect, memo or context reads does nothing useful.
- Expecting memo to block state updates — a child's own state still re-renders it;
React.memoonly guards against prop-driven renders. - Caching cheap derivations instead of moving them — often the better fix is computing during render or extracting a smaller component.
Practice checklist
- Add
useMemoto an expensive filter and watch the console log frequency change. - Pass a fresh callback to a
React.memochild, observe re-renders in the Profiler, then stabilise it withuseCallback. - Profile a real interaction before and after memoising — record the numbers.
- Write one memo with a deliberately missing dependency and observe the stale result; fix the array.
- Find one memo you would remove after measuring, and remove it.
Reusable logic deserves reusable hooks — build some in Building Your Own Hooks.
Frequently Asked Questions
React is a JavaScript library for building user interfaces from reusable components. It manages how your UI updates when data changes, so you describe what the screen should look like and React performs the efficient DOM updates. It is one of the most in-demand frontend skills and powers products from Netflix to Instagram, which makes it an excellent next step after core JavaScript.
You should be comfortable with functions, arrays and objects (map, filter, spread), destructuring, modules and basic async/await. React itself is written in plain JavaScript — JSX is a syntax layer on top. If any of those feel shaky, revisit the JavaScript fundamentals first and this series will go much faster.
React is a library focused only on the UI layer — you choose your own router, data layer and tooling. Angular is a full framework that ships routing, forms, HTTP client and testing conventions as one opinionated package. React's flexibility and huge ecosystem make it the common choice for teams that want to compose their own stack.
A component is a reusable function that returns UI. It receives input through props, keeps private data in state, and can render other components. Pages are composed by nesting small components (navbar, card list, footer) instead of one giant file, which keeps code testable and easy to change.
JSX is a syntax extension that looks like HTML but compiles to React's create() calls — it is a description of UI, not markup the browser parses. Small differences matter: class becomes className, style takes an object, and you embed JavaScript expressions inside curly braces. Once compiled, React turns that description into real DOM nodes.
Props are inputs passed from a parent — they are read-only inside the child, like function arguments. State is private, mutable data owned by a component; updating it triggers a re-render. The rule of thumb: values that come from outside the component are props; values that change over the component's lifetime are state.
Rendering means calling your component function to produce a description of the UI for the current state and props. It does not always mean DOM work — React compares the new description with the previous one (reconciliation) and applies only the differences to the real DOM. This is why updating state 100 times does not mean 100 full page rebuilds.
An effect runs after the component has committed to the screen — after render, not during it. You list dependencies in an array, and React re-runs the effect only when one of them changes. If the effect returns a cleanup function, React runs it before the next re-run and on unmount, which is where you cancel timers, abort fetches and remove listeners.
Use Vite. Create React App is effectively in maintenance mode, while Vite starts instantly (native ES modules) and builds fast, and it is the default choice for new React projects. This series uses Vite for every example — you only need Node.js installed to follow along.
React apps compile to static files (HTML, CSS and JavaScript), so any static host works: Netlify, Vercel, GitHub Pages or Cloudflare Pages. Run the production build, upload the dist folder, and configure a single-page-application fallback so unknown paths load index.html — routing chapters explain why that fallback is required.