React Hooks делают нашу жизнь намного лучше почти во всех отношениях . Но если производительность становится проблемой, все становится немного сложнее. Вы можете писать невероятно быстрые приложения с помощью хуков, но прежде чем вы это сделаете, вам следует знать кое-что.

Стоит ли запоминать?

Реагировать достаточно быстро для большинства случаев использования. Если ваше приложение достаточно быстрое и у вас нет проблем с производительностью, эта статья не для вас. Решение воображаемых проблем с производительностью - это реальная задача, поэтому, прежде чем начинать оптимизацию, убедитесь, что вы знакомы с React Profiler.

Если вы определили сценарии, в которых рендеринг идет медленно, лучше всего подойдет мемоизация.

React.memo - инструмент оптимизации производительности, компонент высшего порядка. Он похож на React.PureComponent, но для функциональных компонентов, а не для классов. Если ваш функциональный компонент отображает тот же результат при тех же реквизитах, React запомнит, пропустит рендеринг компонента и повторно использует последний обработанный результат.

По умолчанию он будет только поверхностно сравнивать сложные объекты в объекте props. Если вы хотите контролировать сравнение, вы также можете предоставить настраиваемую функцию сравнения в качестве второго аргумента.

Нет мемоизации

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

function List({ items }) {
  log('renderList');
  return items.map((item, key) => (
    <div key={key}>item: {item.text}</div>
  ));
}
export default function App() {
  log('renderApp');
  const [count, setCount] = useState(0);
  const [items, setItems] = useState(getInitialItems(10));
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>
        inc
      </button>
      <List items={items} />
    </div>
  );
}

Пример 1: Живая демонстрация

Каждый раз, когда нажимается inc, регистрируются и renderApp, и renderList, даже если для List ничего не изменилось. Если дерево достаточно большое, оно легко может стать узким местом в производительности. Нам нужно уменьшить количество рендеров.

Простая мемоизация

const List = React.memo(({ items }) => {
  log('renderList');
  return items.map((item, key) => (
    <div key={key}>item: {item.text}</div>
  ));
});
export default function App() {
  log('renderApp');
  const [count, setCount] = useState(0);
  const [items, setItems] = useState(getInitialItems(10));
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>
        inc
      </button>
      <List items={items} />
    </div>
  );
}

Пример 2: Живая демонстрация

В этом примере мемоизация работает правильно и сокращает количество отрисовок. Во время монтирования регистрируются renderApp и renderList, но при щелчке по inc регистрируется только renderApp.

Воспоминание и обратный звонок

Внесем небольшие изменения и добавим кнопку inc ко всем List элементам. Остерегайтесь, передача обратного вызова мемоизированному компоненту может вызвать небольшие ошибки.

function App() {
  log('renderApp');

  const [count, setCount] = useState(0);
  const [items, setItems] = useState(getInitialItems(10));

  return (
    <div>
      <div style={{ display: 'flex' }}>
        <h1>{count}</h1>
        <button onClick={() => setCount(count + 1)}>
          inc
        </button>
      </div>
      <List
        items={items}
        inc={() => setCount(count + 1)}
      />
    </div>
  );
}

Пример 3: Живая демонстрация

В этом примере наша мемоизация не работает. Поскольку мы используем встроенную лямбду, для каждого рендера создается новая ссылка, что делает React.memo бесполезным. Нам нужен способ запоминания самой функции, прежде чем мы сможем запоминать компонент.

4. useCallback

К счастью, в React для этого есть два встроенных хука: useMemo и useCallback. useMemo полезен для дорогостоящих вычислений, useCallback полезен для передачи обратных вызовов, необходимых для оптимизированных дочерних компонентов.

function App() {
  log('renderApp');

  const [count, setCount] = useState(0);
  const [items, setItems] = useState(getInitialItems(10));

  const inc = useCallback(() => setCount(count + 1));

  return (
    <div>
      <div style={{ display: 'flex' }}>
        <h1>{count}</h1>
        <button onClick={inc}>inc</button>
      </div>
      <List items={items} inc={inc} />
    </div>
  );
}

Пример 4: Живая демонстрация

В этом примере наша мемоизация снова не работает .renderList вызывается каждый раз, когда inc нажимается. По умолчанию useCallback вычисляет новое значение всякий раз, когда передается новый экземпляр функции. Поскольку встроенные лямбды создают новый экземпляр во время каждого рендеринга, useCallback с конфигурацией по умолчанию здесь бесполезен.

5. useCallback с вводом

const inc = useCallback(() => setCount(count + 1), [count]);

Пример 5: Живая демонстрация

useCallback принимает второй аргумент, массив входных данных, и только если эти входные данные изменятся, useCallback вернет новое значение. В этом примере useCallback будет возвращать новую ссылку каждый раз, когда count изменяется. Поскольку count изменяется во время каждого рендеринга, useCallback будет возвращать новое значение во время каждого рендеринга. Этот код также не запоминается.

useCallback с вводом пустого массива

const inc = useCallback(() => setCount(count + 1), []);

Пример 6: Живая демонстрация

useCallback может принимать в качестве входных данных пустой массив, который вызовет внутреннюю лямбду только один раз и запомнит ссылку для будущих вызовов. Этот код запоминает, один renderApp будет вызываться при нажатии любой кнопки, основная inc кнопка будет работать правильно, но внутренние inc кнопки будут перестать работать правильно .

Счетчик увеличится с 0 до 1 и после этого остановится. Лямбда создается один раз, но вызывается несколько раз. Поскольку при создании лямбда-выражения count равно 0, он ведет себя точно так же, как приведенный ниже код:

const inc = useCallback(() => setCount(1), []);

Основная причина нашей проблемы в том, что мы пытаемся читать и писать из состояния и в одно и то же время. Нам нужен API, предназначенный для этой цели. К счастью для нас, React предоставляет два способа решения проблемы:

useState с функциональными обновлениями

const inc = useCallback(() => setCount(c => c + 1), []);

Пример 7: Живая демонстрация

Сеттеры, возвращаемые useState, могут принимать функцию в качестве аргумента, где вы можете прочитать предыдущее значение данного состояния. В этом примере мемоизация работает правильно, без ошибок.

useReducer

const [count, dispatch] = useReducer(c => c + 1, 0);

Пример 8: Живая демонстрация

useReducer мемоизация в этом случае работает точно так же, как useState. Поскольку dispatch гарантированно имеет одну и ту же ссылку во всех отрисовках, useCallback не требуется, что делает код менее подверженным ошибкам для ошибок, связанных с мемоизацией.

useReducer против useState

useReducer больше подходит для управления объектами состояния, которые содержат несколько вложенных значений или когда следующее состояние зависит от предыдущего. Общий шаблон использования useReducer - с useContext, чтобы избежать явной передачи обратных вызовов в большом дереве компонентов.

Я рекомендую эмпирическое правило: в основном использовать useState для данных, которые не покидают компонент, но если требуется нетривиальный двусторонний обмен данными между родителем и потомками, useReducer - лучший выбор.

Подводя итог, React.memo и useReducer - лучшие друзья, React.memo и useState - братья и сестры, которые иногда ссорятся и создают проблемы, useCallback - ближайший сосед, с которым всегда следует быть осторожным.