Мемоизация `React` - atls/convention GitHub Wiki

Два основных постулата React:

  1. Изменение состояния компонента провоцирует его ререндер, а так же его детей
  2. Ререндер происходит сверху вниз по дереву

Задача - минимизировать количество ненужных рендеров. Этого можно добиться несколькими путями:

Спуск состояния

Из постулатов логично заключить, что чем выше компонент с состоянием по дереву, тем большее количество детей будет ререндерится. Соответственно первая опция - попытаться опустить это состояние до того ребёнка/компонента, которому это состояние действительно необходимо.

Компонентная композиция

Рассмотрим на примере:

export const App = () => {
  let [color, setColor] = useState('red');

  return (
    <div style={{ color }}>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p>Hello, world!</p>
      <ExpensiveTree />
    </div>
  );
}

У нас есть App с состоянием, и есть родительский компонент, который ожидает это же состояние. Тут опустить состояние никак не выйдет. Поэтому вместо этого мы можем скомпоновать состояние и зависящие от него компоненты:

export const App = () => {

  return (
    <ColorPicker>
      <p>Hello, world!</p>
      <ExpensiveTree />
    </ColorPicker>
  );
}
 
const ColorPicker = ({ children }) => {
  let [color, setColor] = useState("red");

  return (
    <div style={{ color }}>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      {children}
    </div>
  );
}

Произошло следующее - мы скомпоновали компоненты, которые зависят от состояния в ColorPicker. При изменении состояния изменяется только div и input. children не изменится так как это всё тот же JSX.

Аналогичный пример можно посмотреть здесь.

Мемоизация

Ну а теперь последняя, но не менее важная часть - мемоизация. Достигается тремя, взаимодополняющими способами:

  1. useMemo - используем для дорогостоящих вычислений, объектов/массивов. Можно и рекомендуется оборачивать им любые Fragment.map() - JSX можно так же мемоизирвать, однако важно делать это только с полным пониманием происходящего.

  2. useCallback - используем для оптимизации функций.

  3. memo - для оборачивания компонентов. Фактически это аналог для useMemo, только можно применять вне компонента:

export const App = memo(({ children }) => (<div>{children}</div>))

Теперь подробнее. Без мемоизации при ререндере React проходится по древу сверху-вниз и проводит рендер, однако, если у компонента не поменялись пропсы, то нет необходимости его ререндерить. Именно это и делает memo - говорит React, что если пропсы предыдущие и от текущего рендера равны, то ререндерить не надо.

Но, "есть нюанас" - сравнение происходит по JS-овскому ===. Примитивы (строки, числа, булеаны) будут "правильно" сравниваться. А вот объекты - объекты, функции, массивы - нет. Их React будет создавать каждый раз при новом рендере. Т.е.:

const a = { key: 'value' }

const b = { key: 'value' }

const c = a === b // c === false

Тут в дело вступают useMemo, useCallback - с помощью них мы можем подсказать React, при изменении какого пропса меняются объекты/сложные вычисления внутри.

const hardToEvaluate = useMemo(() => ..., [a, b])

Главный недостаток подходов мемоизации - код становится сложнее читать. Поэтому сначала стараемся действовать 1 и 2 опциями, а потом переходим к мемоизации.

И последнее - если у вас немемоизированный компонент работает, то должен и мемоизированный. И наоборот. Поэтому прежде чем переходить к мемоизации убедитесь/протестируйте ваш компонент.

Как проверить ререндер

  1. банальный console.log внутри компонента даст вам понять произошел ререндер или нет.
  2. React dev tools расширение для браузеров
⚠️ **GitHub.com Fallback** ⚠️