Оптимизации в React часть 2

ИспользоватьMemo или не использоватьMemo?

Вы могли заметить, что наша кнопка CoolButton неправильно отображает знак (+):

<CoolButton clickHandler={increment}>+</CoolButton>
Вход в полноэкранный режим Выход из полноэкранного режима

Когда мы вставляем что-либо внутрь Jsx-элемента, например CoolButton, мы не показываем это пользователю, а передаем в качестве параметра под названием children.

const CoolButton = React.memo(({ clickHandler,children }) => {
    const handler = () => {
        ReallyImportantCalculation();
        clickHandler();
    };
    return <button onClick={handler}></button>;
  });
Вход в полноэкранный режим Выход из полноэкранного режима

Вместо того чтобы ничего не делать, давайте отобразим дочерние элементы:

return <button onClick={handler}>{children}</button>;
Войти в полноэкранный режим Выход из полноэкранного режима

Как и раньше, давайте добавим немного сложности в наш дизайн.

Вместо того чтобы показывать пользователю знак (+), давайте создадим игру «Кликер», которая будет состоять из кнопки, меняющей свой внешний вид в зависимости от количества нажатий на нее.

Мы можем начать с передачи <img/> вместо знака (+) в наш CoolButton:

<CoolButton clickHandler={increment}>
  <img/>
</CoolButton>
Вход в полноэкранный режим Выход из полноэкранного режима

При нажатии на кнопку мы замечаем, что наша мемоизация была снова потеряна; повторное отображение кнопки при каждом нажатии…

Давайте вспомним, что в JSX <img/> не является html-тегом, это сокращение для React.createElement('img',props, ...children).

Превращаем наш код в:

{createElement(CoolButton,{clickHandler:increment},
  createElement('img',null, null)
)}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь легко понять, в чем именно проблема: выполнение createElement при каждом рендере создает нового ребенка, который передается нашей CoolButton в качестве prop.

Сначала нам нужно убрать создание дочернего элемента из CoolButton:

const CurrentImage = <img/>;
<CoolButton clickHandler={increment}>
  {CurrentImage}
</CoolButton>
Войти в полноэкранный режим Выйти из полноэкранного режима

У вас может возникнуть соблазн поместить CurrentImage вне нашего счетчика, что сработает, но поскольку CurrentImage будет иметь состояние, основанное на нашем счетчике, мы должны использовать другой способ:

const CurrentImage = useCallback(<img/>,[]);
<CoolButton clickHandler={increment}>
  {CurrentImage}
</CoolButton>
Войти в полноэкранный режим Выйти из полноэкранного режима

Как и раньше, на помощь приходит useCallback!
Хотя это выглядит немного странно, так как наш CurrentImage на самом деле не обратный вызов, а значение, которое мы хотим запомнить.

useMemo

(Из документации React)

useMemo, как и useCallback, принимает функцию, которая что-то мемоизирует, и массив зависимостей, который перезапускает эту функцию только при изменении зависимостей, в нашем случае мы хотим мемоизировать JsxElement.

Как мы уже говорили, реквизит Children, который мы передаем нашей CoolButton, меняется при каждом рендере, потому что мы каждый раз создаем новое изображение CurrentImage.

Мы можем использоватьMemo для запоминания CurrentImage и предотвращения повторных рендеров:

const CurrentImage = useMemo(() => <img/>,[]);
<CoolButton clickHandler={increment}>
  {CurrentImage}
</CoolButton>
Войти в полноэкранный режим Выйти из полноэкранного режима

Чтобы сделать это немного интереснее, давайте добавим новое состояние phaseImgUrl, которое сообщит нам, какое изображение мы должны представить для каждой фазы нашего кликера:

const [phaseImgUrl, setPhaseImgUrl] = useState('');
const CurrentImage = useMemo(() => <img src={phaseImgUrl}/>,[phaseImgUrl]);
<CoolButton clickHandler={increment}>
  {CurrentImage}
</CoolButton>
Войти в полноэкранный режим Выход из полноэкранного режима

Вот некоторая дополнительная логика, которая будет обрабатывать изменение фаз при достижении определенного порога:

const phases = [
  "https://media4.giphy.com...phase1",
  "https://media4.giphy.com...phase2",
  "https://media4.giphy.com...phase3",
];

useEffect(() => {
    if (count != null) {
      const phaseThreshold = 30;
      const numPhases = phases.length;
      const nextPhaseImgUrl =
        phases[parseInt(count / phaseThreshold, 10) % numPhases];
      if (nextPhaseImgUrl !== phaseImgUrl) {
        setPhaseImgUrl(nextPhaseImgUrl);
      }
    }
  }, [count]);
Войти в полноэкранный режим Выход из полноэкранного режима

Сначала мы проверяем, действителен ли счетчик, а затем важно убедиться, что фаза отличается от предыдущей, чтобы не вызывать лишних setStates и повторных рендеров.

Вот и все!

В следующей части мы поговорим о дополнительных эффектах и связанных с ними опасностях.

Оцените статью
Procodings.ru
Добавить комментарий