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 может быть очень полезным, а, иногда, единственным решением кейсов, которые на реальных проектах могут встречаться достаточно часто.