ReactReact.memo

React.memo

By default, whenever a React component re-renders, all of its children re-render too — even if their props haven't changed. For small, fast components this is fine. But when a parent re-renders frequently and a child is expensive to render, those extra renders waste CPU time. React.memo is the tool for breaking that chain: it wraps a component and skips the re-render when props are identical.

The Basic API

JSX
import { memo } from 'react'

// Unwrapped — re-renders every time the parent re-renders
function ExpensiveList({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  )
}

// Wrapped — only re-renders when 'items' changes (shallow equality)
const ExpensiveList = memo(function ExpensiveList({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  )
})

// Arrow function syntax
const ExpensiveList = memo(({ items }) => (
  <ul>
    {items.map(item => <li key={item.id}>{item.name}</li>)}
  </ul>
))
Note
React.memo only compares props — not state or context. If a memoized component reads from a context value, it will still re-render when that context changes.
How Shallow Equality Works

By default, React.memo uses shallow equality: it compares each prop with Object.is. Primitive values (strings, numbers, booleans) compare by value. Objects and arrays compare by reference. This distinction is critical:

JSX
const Child = memo(({ user, onClick }) => {
  console.log('Child rendered')
  return <button onClick={onClick}>{user.name}</button>
})

function Parent() {
  const [count, setCount] = useState(0)

  // ✗ New object reference on every render — memo is defeated
  const user = { name: 'Alice', id: 1 }

  // ✗ New function reference on every render — memo is defeated
  const handleClick = () => console.log('clicked')

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>Parent: {count}</button>
      {/* Child re-renders on every parent render because user and handleClick
          are new references each time */}
      <Child user={user} onClick={handleClick} />
    </>
  )
}
Warning
React.memo wrapping a component that receives inline objects, arrays, or functions as props provides zero benefit — those props are new references on every render, so shallow equality always fails.
The Fix: useMemo + useCallback

To make React.memo effective, stabilize the prop references with useMemo (for objects/arrays) and useCallback (for functions):

JSX
import { memo, useState, useMemo, useCallback } from 'react'

const Child = memo(({ user, onClick }) => {
  console.log('Child rendered')
  return <button onClick={onClick}>{user.name}</button>
})

function Parent() {
  const [count, setCount] = useState(0)

  // ✓ Same reference between renders (unless id changes)
  const user = useMemo(() => ({ name: 'Alice', id: 1 }), [])

  // ✓ Same function reference between renders
  const handleClick = useCallback(() => console.log('clicked'), [])

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>Parent: {count}</button>
      {/* Now Child does NOT re-render when count changes */}
      <Child user={user} onClick={handleClick} />
    </>
  )
}
Custom Comparison Function

The second argument to React.memo is a comparison function. It receives the previous and next props and returns true to skip the re-render, or false to allow it. Think of it as the inverse of shouldComponentUpdate:

JSX
const UserCard = memo(
  function UserCard({ user, highlightColor }) {
    return (
      <div style={{ background: highlightColor }}>
        <h3>{user.name}</h3>
        <p>{user.email}</p>
      </div>
    )
  },
  // Custom comparison: only re-render if user.id or highlightColor changes
  // (ignore changes to other user properties like lastSeen, etc.)
  (prevProps, nextProps) => {
    const sameUser  = prevProps.user.id === nextProps.user.id
    const sameColor = prevProps.highlightColor === nextProps.highlightColor
    // return true  → SKIP re-render (props are "equal enough")
    // return false → ALLOW re-render
    return sameUser && sameColor
  }
)
Warning
Custom comparison functions are easy to get wrong — a bug that returns true when props have meaningfully changed causes stale UI that's hard to debug. Use sparingly, and always prefer fixing the prop reference stability instead.
When React.memo Helps
  • Parent re-renders frequently for reasons unrelated to the child (a text input updating, a timer ticking, an animation running).

  • Child renders are expensive — long lists, complex SVGs, heavy calculations in the render body.

  • Props are stable — primitive values, or objects/functions stabilized with useMemo/useCallback.

  • Pure presentational leaf components — no state, no side effects, just props → JSX.

When React.memo Doesn't Help
  • Props always change — if every render produces different prop values, memo just adds the overhead of comparison for no benefit.

  • The component is fast — if the render takes under 1 ms, the memo comparison overhead is comparable to just re-rendering.

  • The component reads changing contextReact.memo does not prevent context-driven re-renders.

  • State or children — memo only checks props; if the wrapped component has its own useState, state changes still trigger re-renders.

A Practical Example: Row in a Large List

JSX
import { memo, useState, useCallback } from 'react'

// Individual row — potentially re-created 1000 times per parent render without memo
const TodoRow = memo(function TodoRow({ todo, onToggle }) {
  return (
    <li
      style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
      onClick={() => onToggle(todo.id)}
    >
      {todo.text}
    </li>
  )
})

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Buy milk', done: false },
    { id: 2, text: 'Walk the dog', done: true },
    // ... imagine 1000 items
  ])
  const [filter, setFilter] = useState('all')

  // Stable callback — does not change between renders
  const handleToggle = useCallback((id) => {
    setTodos(prev =>
      prev.map(t => t.id === id ? { ...t, done: !t.done } : t)
    )
  }, [])

  const visible = todos.filter(t =>
    filter === 'all' || (filter === 'done') === t.done
  )

  return (
    <>
      {/* Changing the filter causes TodoList to re-render, but each TodoRow
          only re-renders if its own todo object changed */}
      <select value={filter} onChange={e => setFilter(e.target.value)}>
        <option value="all">All</option>
        <option value="done">Done</option>
        <option value="pending">Pending</option>
      </select>
      <ul>
        {visible.map(todo => (
          <TodoRow key={todo.id} todo={todo} onToggle={handleToggle} />
        ))}
      </ul>
    </>
  )
}
Tip
Before adding React.memo, use the React DevTools Profiler to record a slow interaction. The flame chart shows exactly which components are re-rendering and how long they take. Optimise based on data, not intuition — many optimisations are premature.
Quick Reference
  • memo(Component) — skip re-render when props are shallowly equal.

  • memo(Component, (prev, next) => bool) — custom equality; return true to skip.

  • Inline objects/arrays/functions defeat memo — stabilize with useMemo/useCallback.

  • Memo only checks props — state, ref, and context changes still cause re-renders.

  • Profile first. Only memoize components that are measurably slow.