Memoization
The rendering chapter is the mental model this one stands on: when state changes, React re-runs the component and everything below it, and that is usually fine because most renders are fast. Sometimes it isn't fine. A calculation at the top of a component might take long enough to notice, or a subtree of components might be expensive enough that re-rendering all of them on every keystroke makes the app feel sluggish.
For those measured cases React offers three tools, useMemo, memo, and useCallback, and all three do variations of one thing: they let React remember a result from the previous render and skip redoing the work. To use any of them well, you first need to know how JavaScript decides whether two things are "the same."
Value types, reference types, and referential equality
JavaScript splits values into two families. Value types (also called primitives) are booleans, numbers, strings, and the ones you meet less often: undefined, null, symbol, and bigint. Two of them are equal when they hold the same value of the same type: 1 === 1 is true. Reference types (also called complex types) are objects, arrays, and functions. Two of them are equal only when they are literally the same thing in memory. Looking identical counts for nothing.
const a = { name: 'Joe' }
const b = { name: 'Joe' }
console.log(Object.is(a, b)) // false: two separate objects in memory
const c = a
console.log(Object.is(a, c)) // true: both point at the same objectObject.is is what React uses under the hood for these comparisons, and it behaves almost identically to === here. Even {} !== {}: two empty object literals are two distinct allocations, so they fail the check. This is called referential equality, and it is the root concept of this whole chapter.
Now connect it to rendering. A component is a function, and every render re-runs its body from the top. Any object, array, or function created in that body is brand new on every render:
function App() {
const styles = { backgroundColor: 'black' } // fresh object each render
function increment() { /* ... */ } // fresh function each render
// ...
}The contents may be identical from one render to the next, but the references never are. Any comparison that asks "did this prop change?" by checking Object.is will answer yes every single time. Everything that follows is about controlling when those references change.
useMemo: skip an expensive calculation
The first use of memoization has nothing to do with references yet. A calculation sitting at the top level of a component runs on every render, even renders triggered by state the calculation never touches. If sorting a large product list takes a noticeable slice of time, a counter button elsewhere in the same component turns janky, because every click re-runs the sort for no reason.
useMemo fixes that with the same shape as an effect: a function plus a dependency array. React calls the function, remembers what it returns, and on later renders hands back the remembered value without calling the function again, as long as nothing in the dependency array has changed.
import { useMemo, useState } from 'react'
function ProductList({ productsData }) {
const [count, setCount] = useState(0)
const sortedProducts = useMemo(() => {
return [...productsData].sort((a, b) => a.name.localeCompare(b.name))
}, [productsData])
// ...
}The dependency array holds everything the calculation reads: here, only productsData. Changing count re-renders the component, but React sees that productsData is the same and skips the sort entirely. Only a new productsData array triggers a recalculation. In the course's demo, that took the sort from a visible lag on every click down to zero milliseconds on renders that didn't touch the data.
memo: skip a child's re-render
useMemo caches a value inside one component. memo operates a level up: it can skip re-rendering an entire component. It is a higher-order component, a function that takes your component and returns an upgraded version of it. You'll often see it written React.memo in older codebases; the modern import is named.
import { memo } from 'react'
function Product({ name, styles }) {
// an expensive component body
}
export default memo(Product)The upgraded component compares its previous props to its incoming props. If every prop is the same, React skips re-rendering it for that parent render, and the subtree it would have rendered is skipped with it (with exceptions, covered below). In the course's demo app, wrapping one component in memo cut a state change from thirty-one renders down to one, because the whole untouched subtree got skipped in a single decision.
"The same" means shallow equality: each prop is compared with Object.is. A string or number prop compares by value and behaves the way you'd expect. And that is exactly where the trouble starts.
Why memo silently stops working
Pass a memo-wrapped component an object prop, and the optimization quietly dies:
function App() {
const [darkMode, setDarkMode] = useState(false)
const [count, setCount] = useState(0)
const styles = {
backgroundColor: darkMode ? '#222' : '#fff',
color: darkMode ? '#fff' : '#000',
}
return <Product styles={styles} /* ... */ />
}Every time count changes, App re-renders and builds a fresh styles object. The values inside are identical, but referential equality doesn't care: Object.is(oldStyles, newStyles) is false, memo concludes the props changed, and Product re-renders anyway. Nothing errors, nothing warns. The memoization does nothing at all.
This is the second job of useMemo: holding on to a reference between renders so a comparison can succeed. Wrap the object creation and give it the one dependency it actually varies with:
const styles = useMemo(() => ({
backgroundColor: darkMode ? '#222' : '#fff',
color: darkMode ? '#fff' : '#000',
}), [darkMode])Now styles is the same object, by reference, on every render until darkMode changes. The memo check passes, and Product stays skipped when only the counter moves. The two tools compose: memo on the child asks "did the props change?", and useMemo in the parent makes sure the answer can be no.
children is a prop like any other, and this catches people out. JSX children written in the parent are a fresh element object on every parent render, so <Card><Chart /></Card> hands Card a new children reference every time and memo on Card never succeeds, however stable the other props are. The structural pattern rendering describes works from the other side: when the element is created above the component that owns the state, that component's re-render sees the same element object and skips the branch without any memoization.
What memo doesn't skip
memo governs only the props arriving from the parent. A memoized component still re-renders when its own state changes and when a context it reads updates. Context reaches past a skipped subtree too: a descendant consuming a changed context re-renders even though the memoized wrapper above it was skipped.
useCallback: useMemo for functions
Functions are reference types too, so a callback prop breaks memo in exactly the same way. Define function increment() {...} in a component body, pass it down, and the child receives a brand-new function reference on every render of the parent. useCallback exists for precisely this: it memoizes the function itself.
const [selectedProduct, setSelectedProduct] = useState(null)
const chooseProduct = useCallback((id) => {
console.log('New product selected')
setSelectedProduct(id)
}, [])Pass chooseProduct down to a memo-wrapped child and the optimization holds: useCallback(fn, deps) keeps the same function reference between renders until a dependency changes. The relationship to useMemo is close: with useMemo the remembered value is whatever your function returns, while with useCallback the remembered value is the function you pass in.
One detail to carry with you: state setter functions like setSelectedProduct already have a stable identity, React guarantees it. Passing a raw setter down as a prop is safe without useCallback; the hook earns its keep when you wrap the setter in your own function that does extra work first.
A stable function reference also matters beyond memo. If a function appears in a useEffect dependency array, a fresh reference every render means the effect re-runs every render, as effects covers. useCallback quiets that too.
Measure first
Confirm there is a performance problem before reaching for any of this. Rendering makes the case and lays out the workflow: most renders are cheap, a render that changes nothing never reaches the DOM, and the Profiler with a throttled CPU is how you find the ones that do cost something. Memoizing a trivial calculation like data.length costs more than it saves, because the caching and comparison are themselves work. Fix only what you can measure, and fix slow renders before you spend any energy reducing the number of renders.
Version note
The React Compiler, which reached 1.0 in October 2025, automates most of what this chapter covers: it analyzes components at build time and inserts memoization automatically, so hand-written useMemo, useCallback, and memo become largely unnecessary in projects that adopt it. The concepts remain worth understanding, both because existing codebases are full of these hooks and because knowing what the compiler does for you makes its behavior legible instead of magical.
useMemo lets React remember the answer and reuse it until the inputs change. memo does something similar for a whole component: if its props are the same as last time, React skips rendering it. The tricky part is that two objects or functions that look identical still count as different in JavaScript, which is why these tools sometimes need each other. And before using any of them, make sure something is actually slow.
Next up: Code splitting, where the other lever, bundle size, gets its turn.

