Command Palette

Search for a command to run...

GitHub
Блог
Previous

useMemo, useCallback и React.memo на простых примерах

Разбираем зачем нужны эти три инструмента оптимизации и как они работают вместе. Минимум теории, максимум кода.

Проблема лишних рендеров

По умолчанию, когда родительский компонент обновляется — его дети тоже рендерятся. Даже если пропсы не поменялись.

function Child({ value }) {
  console.log("render child")
  return <div>{value}</div>
}
 
function Parent() {
  const [count, setCount] = useState(0)
 
  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <Child value="static text" />
    </>
  )
}
  • Каждый клик вызывает setCount.
  • Parent ререндерится — и Child тоже.
  • Но value у Child не менялся.

React.memo

Чтобы Child не рендерился зря, оборачиваем его в React.memo.

const Child = React.memo(function Child({ value }) {
  console.log("render child")
  return <div>{value}</div>
})

Теперь Child обновится только если value реально изменится.


Проблема с функциями

JS считает каждую функцию новой. Даже если тело то же самое.

function Parent() {
  const [count, setCount] = useState(0)
 
  const handleClick = () => console.log("clicked")
 
  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <Child onClick={handleClick} />
    </>
  )
}
  • Каждый рендер создаёт новый handleClick.
  • Для Child это "новые пропсы".
  • React.memo думает: пропсы изменились → ререндер.

useCallback

Фиксим с useCallback, чтобы ссылка на функцию оставалась той же.

const handleClick = useCallback(() => {
  console.log("clicked")
}, [])

Теперь:

  • пока зависимости не изменились → ссылка та же
  • React.memo видит старый onClick → не ререндерит ребёнка

Проблема с тяжёлыми вычислениями

Допустим у нас список чисел и мы считаем сумму.

function Numbers({ items }) {
  const total = items.reduce((a, b) => a + b, 0)
  return <div>Sum: {total}</div>
}

Даже если items не менялся, при каждом рендере будет заново пересчёт.


useMemo

С useMemo пересчёт будет только если изменился items.

const total = useMemo(() => {
  console.log("heavy calc...")
  return items.reduce((a, b) => a + b, 0)
}, [items])

Теперь:

  • items не меняется → берём закэшированное значение
  • items изменился → пересчитываем

Всё вместе

Обычно схема такая:

  • React.memo на ребёнке
  • useCallback для функций
  • useMemo для вычислений
const Child = React.memo(function Child({ value, onClick }) {
  console.log("render child")
  return <button onClick={onClick}>{value}</button>
})
 
function Parent({ items }) {
  const total = useMemo(() => items.reduce((a, b) => a + b, 0), [items])
  const handleClick = useCallback(() => console.log("clicked"), [])
 
  return <Child value={total} onClick={handleClick} />
}

Когда использовать

  • если есть заметные тормоза от вычислений → useMemo
  • если пропсы-функции ломают React.memouseCallback
  • если дорогой в рендере компонент не должен перерисовываться без причины → React.memo

Итог

  • React.memo — не рендерит ребёнка, если пропсы те же
  • useCallback — сохраняет одну и ту же ссылку на функцию
  • useMemo — сохраняет результат вычисления

Эти штуки нужны точечно, не "на всякий случай". Сначала пиши код просто. Потом смотри профайлер и добавляй оптимизации там, где реально видно проблему.