React
May 22, 2023

useEffect vs useLayoutEffect

Каждый React-разработчик использует при решении повседневных задач хук useEffect. Однако, как показывает моя практика проведения собеседований, про useLayoutEffect слышали уже не так много людей, а до конца понимают, как именно он работает - ещё меньше. Сегодня, мы разберёмся в различиях между этими двумя хуками, поймём в какой момент жизненного цикла вызывается каждый из них и узнаем, когда лучше использовать useEffect, а когда - useLayoutEffect. Статья подойдёт как опытным, так и начинающим Frontend-разработчикам.

Немного теории

useEffect

Итак, поговорим немного о наших хуках.
Как ты наверняка знаешь, хуки эффекта пришли в React после появления функциональных компонентов. Необходимость в них была вызвана тем, что функциональные компоненты не имеют собственного состояния и, что важно, методов жизненного цикла, в отличие от классовых компонентов. То есть, без хуков, функциональные компоненты способны только на рендер JSX.

useEffect - хук эффекта, который аналогичен комбинации componentDidMount, componentDidUpdate и componentWillUnmount. Он принимает в себя два параметра - callback и массив зависимостей. Массив зависимостей представляет из себя набор переменных, на изменение которых подписывается наш хук, и, соответственно, при их изменении вызвается переданный нами callback. Посмотрим за работой хука на примере:

import React, { useState, useEffect } from "react";

export const Effect: React.FC = () => {  
  const [counter, setCounter] = useState(0);
  
  useEffect(() => {
    console.log(counter);  
  }, [counter]);
  
  const incrementCounter = () => setCounter(counter + 1);
  
  return (    
    <div>      
      <button onClick={incrementCounter}>INC</button>    
    </div>  
  );
};


В данном примере у нас есть счётчик (переменная counter), функция увеличения счётчика и эффект, который реагирует на это изменение. Что же у нас будет в консоли ?
Для начала - наш эффект сработает как componentDidMount и после первого рендера выведет в консоль начальное значение перменной counter, а именно - ноль. Каждое нажатие кнопки будет изменять нашу переменную стейта и эффект будет работать по принципу componentDidUpdate. Важно запомнить, что useEffect работает в асинхронном режиме и вызывается после того, как в DOM были внесены изменения.
С первыми двумя методами мы определились. Но как же вызвать componentWillUnmount ?
Для этого, callback который мы передаём в useEffect должен иметь return. Немного изменим наш код:

import React, { useState, useEffect } from "react";

export const Effect: React.FC = () => {  
  const [counter, setCounter] = useState(0);
  
  useEffect(() => {
    console.log("componentDidMount/Update", counter);
    
    return () => console.log("componentWillUnmount", counter);  
  }, [counter]);
  
  const incrementCounter = () => setCounter(counter + 1);
  
  return (    
    <div>      
      <button onClick={incrementCounter}>INC</button>    
    </div>  
  );
};

Что же произойдет, если мы запустим этот компонент и нажмём кнопку ?
Сначала - произойдет рендер компонента, после чего, useEffect сработает как componentDidMount и выведет в консоль "componentDidMount/Update 0".
После нажатия на кнопку, произойдет размонтирование компонента и выполнится callback, который мы описали в return, то есть, наш эффект сработает как componentWillUnmount и выведет в консоль "componentWillUnmount 0". Почему именно ноль ? Потому внесение изменений в состояние компонента происходит после его размонтирования.
Ну и наконец, после обновления нашего компонента мы в консоли увидим запись - "componentDidMount/Update 1"

useLayoutEffect

С работой useEffect разобрались, теперь перейдём к useLayoutEffect.
У него есть всего два отличия от useEffect:

1) Синхронное выполнение
2) Выполнение хука происходит ДО рендера

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

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

Итак, теперь посмотрим наглядно, когда лучше использовать useEffect, а когда - useLayoutEffect.
Самый главный, на мой вгляд профит, можно получить от useLayoutEffect в том случае, когда нам необходимо вычислить и установить какие-нибудь стартовые параметры для компонентов, которые мы хотим отрендерить. Посмотрим на пример:

export const Effect: React.FC = () => {  
  const [bgColor, setBgColor] = useState("red");
  
  useEffect(() => {    
    setBgColor("blue");  
  }, []);
  
  return (    
    <div style={{ width: "200px", height: "200px", background: bgColor }}>
    </div>  
  );
};


Вроде бы всё просто, мы хотим, чтобы наш блок при рендере был синего цвета. И действительно, если запустить этот код - блок будет синим, однако, если внимательно посмотреть на блок во время рефреша страницы, то мы увидим кратковременное мигание блока с красного на синий цвет. Это обусловлено тем, что useEffect вызывается после первичного рендера. Этого можно избежать, заменив в данном примере useEffect на useLayoutEffect.

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

useEffect же лучше использовать для различных манипуляций, которые не требуют срочного вмешательства в структуру DOM - к примеру, сетевых запросов, логирования, сложных вычислений, необходимых для обновления компонента и т.д.

Заключение

Мы выяснили, что useLayoutEffect - неплохое средство оптимизации компонента. С помощью него можно предотвратить лишний update компонента, предварительно произвести вычисления или засетить нужные значения в state.

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