Lesson 11 of 12

Performance: memo, useMemo & useCallback

You will learn how to use memo, useMemo and useCallback to avoid unnecessary re-renders. React is fast by default, so use these only where you have measured a problem.

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

// memo skips re-rendering a component when its props are shallowly equal.
const Row = memo(function Row({ item, onSelect }) {
  console.log('rendering', item.id)
  return <li onClick={() => onSelect(item.id)}>{item.name}</li>
})

function List({ items }) {
  const [query, setQuery] = useState('')
  const [selected, setSelected] = useState(null)

  // useMemo: skip an expensive calculation when its inputs have not changed.
  const filtered = useMemo(
    () => items.filter(i => i.name.toLowerCase().includes(query.toLowerCase())),
    [items, query]
  )

  // useCallback: keep the SAME function identity between renders, so the
  // memoized Row above actually stays memoized.
  const handleSelect = useCallback(id => setSelected(id), [])

  return (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <ul>
        {filtered.map(item => (
          <Row key={item.id} item={item} onSelect={handleSelect} />
        ))}
      </ul>
    </>
  )
}

// Without useCallback, handleSelect is a NEW function every render, so every
// Row gets a new prop, so memo never skips anything. memo and useCallback
// only work as a pair.
JSX
import { useState, lazy, Suspense } from 'react'

// The cheapest optimizations are structural — no hooks required.

// 1. Move state DOWN so fewer components re-render.
function SlowPage() {
  const [text, setText] = useState('')     // every keystroke re-renders ExpensiveTree
  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <ExpensiveTree />
    </>
  )
}

function FastPage() {
  return (
    <>
      <TextField />          {/* state lives here now */}
      <ExpensiveTree />      {/* untouched by typing */}
    </>
  )
}

// 2. Pass expensive subtrees as children so they keep the same element identity.
function Wrapper({ children }) {
  const [count, setCount] = useState(0)
  return (
    <div onClick={() => setCount(count + 1)}>
      {count}
      {children}             {/* does not re-render when count changes */}
    </div>
  )
}

// 3. Lazy-load routes and heavy components
const Chart = lazy(() => import('./Chart'))

function Report() {
  return (
    <Suspense fallback={<Spinner />}>
      <Chart />
    </Suspense>
  )
}
JSX
import { useState, useDeferredValue, useTransition, memo } from 'react'

// useDeferredValue: keep the input responsive while a heavy list catches up.
function SearchPage({ allItems }) {
  const [query, setQuery] = useState('')
  const deferredQuery = useDeferredValue(query)
  const isStale = query !== deferredQuery

  return (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <div style={{ opacity: isStale ? 0.6 : 1 }}>
        <ExpensiveResults query={deferredQuery} items={allItems} />
      </div>
    </>
  )
}

const ExpensiveResults = memo(function ExpensiveResults({ query, items }) {
  const results = items.filter(i => i.name.includes(query))
  return <ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul>
})

// useTransition: mark an update as low priority so urgent ones interrupt it.
function TabBar() {
  const [tab, setTab] = useState('home')
  const [isPending, startTransition] = useTransition()

  function select(next) {
    startTransition(() => setTab(next))    // the click feedback stays instant
  }

  return (
    <>
      <button onClick={() => select('reports')}>Reports</button>
      {isPending && <Spinner />}
      <TabPanel tab={tab} />
    </>
  )
}
Finished reading?Mark it done to track your progress.

Tip: use the ← and → keys to move between lessons.