useMemo, useCallback & memo
How re-rendering works, when memoisation helps, and what the React Compiler does for you.
React is fast by default, but every state change re-renders a component and all of its children. Usually that's cheap. Occasionally a component does expensive work or a large list re-renders on every keystroke, and you want to skip unnecessary work. That's what memoisation is for: remembering a previous result and reusing it when the inputs haven't changed.
React gives you three tools — useMemo, useCallback and memo — and the React Compiler can apply them automatically.
First: measure, don't guess#
Before optimising, confirm there's a problem. Use the React DevTools Profiler (covered in Performance & DevTools) or a quick timer:
If it takes well under a millisecond, memoising won't help. As a rule of thumb, look at anything that takes more than ~1 ms or causes visible lag while typing. Test with a production build and CPU throttling in DevTools — development mode is slower.
useMemo: cache a calculation#
Without useMemo, toggling the theme would also run the 50 ms filter. With it, React compares the dependencies (products, query) using Object.is; if they're the same as last render, it returns the cached result.
The calculation function must be pure — no side effects, just compute and return.
memo: skip re-rendering a component#
memo wraps a component so it only re-renders when its props change:
A memoised component still re-renders when its own state or a context it uses changes.
The referential equality trap#
Here's where people get stuck. This memo does nothing:
[] !== [] and (() => {}) !== (() => {}) in JavaScript. Every render creates new values, so memo's comparison always fails:
The fix is to keep those values stable between renders.
useCallback: cache a function#
useCallback(fn, deps) returns the same function until a dependency changes. It's exactly useMemo(() => fn, deps):
Now clicking the counter doesn't re-render ProductList — both props are the same references as before.
Tip: if your callback needs the current state, use an updater function (
setItems(prev => ...)) so you can leave the state out of the dependency array and keep the callback stable.
Stable effect dependencies#
Memoisation also matters for effects. An object or function created during render changes every time, so an effect that depends on it runs every render:
Often the best fix isn't useMemo at all — move the object inside the effect and depend on the primitive:
Cheaper alternatives to memoising#
Before memoising, try restructuring:
- Move state down. If only a small part of the page uses some state, put that state in a smaller component so fewer things re-render.
- Pass children. A component that receives JSX as
childrendoesn't re-render that JSX when its own state changes:
- Keep renders pure and avoid unnecessary effects that set state.
- Virtualise long lists (render only visible rows) with a library like TanStack Virtual.
The React Compiler#
Manually adding useMemo, useCallback and memo is tedious and easy to get wrong. React Compiler (stable since v1.0) is a build-time tool that analyses your components and adds memoisation automatically — often more precisely than you would by hand.
In a Vite project, install the Babel plugin and add React's preset:
Next.js has a reactCompiler: true option, and Expo enables it by default in new apps. With the compiler on:
- You can write plain components and rely on it for most memoisation.
- Your code must follow the Rules of React — pure renders, no mutating props/state, hooks at the top level. The
eslint-plugin-react-hooksrecommended config includes compiler-powered lint rules that flag violations. - Components marked with a ✨ "Memo" badge in React DevTools have been optimised.
- Existing
useMemo/useCallbackcalls are fine to leave in place; the compiler works alongside them.
When to memoise manually#
Even with the compiler, it helps to understand memoisation. Reach for it when:
- A calculation is measurably slow (> ~1 ms) and its inputs rarely change.
- You pass props to a
memocomponent and need them to be stable. - A value is used as an effect dependency and must not change every render.
- You're writing a custom hook that returns functions or objects others will depend on.
Don't wrap everything "just in case" — memoising has its own cost and makes code harder to read.
Common mistakes#
- Memoising cheap calculations, adding complexity for no gain.
- Wrapping a component in
memobut passing it new inline objects/functions every render. - Leaving dependencies out of the array, causing stale values (let the linter help).
- Using
useMemofor side effects — useuseEffectinstead. - Expecting
useMemoto be a guaranteed cache; React may discard it (e.g. during development). It's a performance hint, not a storage mechanism.
What's next#
Sometimes components don't just render slowly — they crash. Next: error boundaries, so one broken widget doesn't take down the whole page.
Check your understanding
Quick quiz
1.What does
useMemo(() => compute(a, b), [a, b])do?2.A child is wrapped in
memo, but it still re-renders every time the parent renders. A likely cause?3.What does the React Compiler do?
Finished reading?
Mark this lesson complete to track your progress.