Skip to content

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.

jsx
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 object

Object.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:

jsx
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.

jsx
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.

jsx
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:

jsx
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:

jsx
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.

jsx
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.

Mechanically, useMemo stores the returned value and the dependency array in the component's slot in React's internal tree. On the next render it walks the new dependency array against the stored one, comparing element by element with Object.is; any mismatch reruns the function and replaces both. That makes the dependency array itself subject to referential equality: an object dependency that gets a fresh reference every render defeats the memoization from the inside, which is why stabilization tends to cascade upward through a component.

memo runs the same Object.is comparison across each key of the props object. It accepts an optional second argument, a function receiving the old and new props so you can define equality yourself, and the React docs are right that you will almost never need it.

The sharper edge is that the comparison is all-or-nothing per component: one unstable prop among ten stable ones re-renders the child every time, so a memo wrapper is only as good as the least stable prop it receives, and a wrapper that never fires shows up in the Profiler as a subtree that keeps re-rendering.

Keep two boundaries in mind. useCallback(fn, deps) is literally useMemo(() => fn, deps), so anything true of one is true of the other. And useMemo is a performance hint rather than a semantic guarantee: React reserves the right to throw cached values away, for example when a component suspends during its initial mount, or in development whenever you edit the file, so code must stay correct if the function reruns. Store things that must survive re-renders without exception in state or a ref. The same stable-identity reasoning applies to context provider values, which that chapter's deep dive already covers.

JunoStable references skip work React re-runs a component's whole body on every render, so any slow calculation in there runs again and again. 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.

JunoStable references skip work Wrap an expensive calculation in useMemo with its real inputs as dependencies, and wrap an expensive child in memo to skip it when props haven't changed.

Then remember that objects and functions created in a component body are new references every render, so a memo child receiving them re-renders anyway: stabilize objects with useMemo and functions with useCallback. State setters are already stable.

Measure with a throttled CPU first; most re-renders cost nothing worth optimizing.

JunoStable references skip work Everything here reduces to Object.is: dependency arrays are compared element-wise, memo compares props shallowly, and one unstable reference anywhere breaks the chain. useCallback(fn, deps) is useMemo(() => fn, deps), and useMemo is a hint React may discard, so keep correctness independent of the cache.

The React Compiler now automates this analysis at build time; the manual hooks matter for existing code and for understanding what the compiler is doing on your behalf.

Next up: Code splitting, where the other lever, bundle size, gets its turn.