React.memo & Shallow Comparison

Hard

React.memo is a higher-order component that memoizes a functional component, skipping re-renders when props have not changed. It uses shallow comparison by default and can accept a custom comparator. It is the primary tool for preventing unnecessary re-renders in child components.

Interactive Visualization

Key Points

  • Wraps a component to skip re-render when props are shallowly equal
  • Shallow comparison: checks top-level property references, not deep equality
  • Must be paired with useCallback/useMemo for object and function props
  • Custom comparator function can override the default shallow comparison
  • Not useful if the component re-renders due to its own state or context changes
  • Has overhead from the comparison, so only use for components that re-render often with the same props

Code Examples

Memoizing an Expensive Component

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

interface DataGridProps {
  rows: Array<{ id: string; values: number[] }>
  onRowClick: (id: string) => void
}

const DataGrid = memo(function DataGrid({ rows, onRowClick }: DataGridProps) {
  return (
    <table>
      <tbody>
        {rows.map((row) => (
          <tr key={row.id} onClick={() => onRowClick(row.id)}>
            {row.values.map((v, i) => <td key={i}>{v}</td>)}
          </tr>
        ))}
      </tbody>
    </table>
  )
})

function Dashboard() {
  const [selectedId, setSelectedId] = useState<string | null>(null)
  const [rows] = useState(() => generateRows())

  const handleRowClick = useCallback((id: string) => {
    setSelectedId(id)
  }, [])

  return (
    <div>
      <p>Selected: {selectedId}</p>
      {/* DataGrid skips re-render when selectedId changes */}
      <DataGrid rows={rows} onRowClick={handleRowClick} />
    </div>
  )
}

function generateRows() {
  return Array.from({ length: 100 }, (_, i) => ({
    id: String(i),
    values: [i, i * 2, i * 3],
  }))
}

React.memo plus useCallback ensures DataGrid only re-renders when rows or onRowClick actually change, not when selectedId changes

Custom Comparator

import { memo } from 'react'

interface ChartProps {
  data: number[]
  width: number
  height: number
  label: string
}

const Chart = memo(
  function Chart({ data, width, height, label }: ChartProps) {
    return <canvas width={width} height={height} data-label={label} />
  },
  (prevProps, nextProps) => {
    // Only re-render if data or dimensions change, ignore label
    return (
      prevProps.data === nextProps.data &&
      prevProps.width === nextProps.width &&
      prevProps.height === nextProps.height
    )
  }
)

A custom comparator lets you control exactly which prop changes should trigger a re-render, ignoring irrelevant props

Common Mistakes

  • Wrapping every component in memo without profiling to see if it actually helps
  • Passing new object or function references on every render which defeats memoization
  • Using memo on components that almost always receive different props

Interview Tips

  • Explain that memo is an optimization and should be guided by profiling
  • Know the full chain: memo + useCallback + useMemo work together
  • Discuss when the React Compiler will make manual memoization unnecessary

Related Concepts