Menu

useLayoutEffect в React: измерение до отрисовки браузером

useLayoutEffect работает как useEffect, но выполняется сразу после того, как React обновил DOM, и до того, как браузер отрисует страницу. Используйте его, чтобы измерить элемент и подправить макет без видимого мерцания, а для всего остального оставьте useEffect.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

useLayoutEffect это версия useEffect, которая выполняется раньше: сразу после того, как React обновил DOM, но до того, как браузер отрисует новый кадр. Код в нём может измерять элементы и менять макет, а пользователь видит только итоговый результат. Пока он работает, отрисовка заблокирована, поэтому используйте его, только когда нужен именно такой момент.

Эффекты объявлены в порядке effect, layout effect, но консоль при каждом рендере сначала выводит layout effect. Порядок объявления между двумя хуками не важен; важно время их выполнения.

Синтаксис

Сигнатура идентична useEffect: функция настройки, необязательная очистка и массив зависимостей.

useLayoutEffect(() => {
    // runs after the DOM update, before paint
    return () => {
        // cleanup, before the next run and on unmount
    };
}, [dependencies]);

Всё, что вы знаете о зависимостях и очистке из useEffect, применимо. Единственная разница в том, когда React его вызывает.

Когда выполняется каждый

После рендера React обновляет DOM, а затем делает три вещи в фиксированном порядке:

  1. Синхронно выполняет все очистки layout-эффектов и сами layout-эффекты.
  2. Даёт браузеру отрисовать страницу. Если layout-эффект установил состояние, React сначала рендерит снова, всё ещё до отрисовки.
  3. Выполняет обычные эффекты, обычно после отрисовки.

По всему дереву каждый layout-эффект выполняется раньше любого обычного эффекта, а дети выполняются раньше родителей, потому что родитель может закончить только тогда, когда его дети на месте:

Консоль показывает сначала оба layout-эффекта, Child, затем Parent, а потом оба обычных эффекта в том же порядке. key={n} заставляет каждый клик монтировать свежий Parent, поэтому вы каждый раз получаете те же четыре строки. Добавьте в каждый эффект очистку, которая что-то выводит, и посмотрите, как старые выполняются раньше новых.

Проблема мерцания

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

  1. Отрендерить подсказку где-нибудь, чтобы её можно было измерить.
  2. Измерить её, вычислить настоящую позицию, установить состояние, отрендерить снова.

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

Наведите на верхнюю кнопку: места над ней нет, поэтому подсказка появляется снизу. Наведите на нижнюю, и она появится сверху. Консоль сообщает, какую сторону выбрало измерение. Замените useLayoutEffect на useEffect и наведите снова: теперь подсказка может на один кадр мелькнуть вверху блока, прежде чем переместиться. На быстрой машине это часто слишком быстро, чтобы заметить, и именно поэтому такую ошибку выпускают, а потом о ней сообщают с более медленных телефонов.

offsetHeight, offsetTop и getBoundingClientRect() заставляют браузер рассчитать макет, поэтому их чтение в layout-эффекте даёт настоящие значения DOM, который React только что записал.

Измерение элемента

Тот же шаблон работает для любого размера, от которого зависит рендер: прочитайте его через ref в layout-эффекте, сохраните в состоянии, рендерите с ним.

Нажмите кнопку, и строка с шириной обновится в том же кадре, что и текст. Страница никогда не показывает новый текст рядом со старой шириной, потому что второй рендер происходит до отрисовки. Уберите [text] из массива зависимостей, и измерение будет выполняться после каждого рендера, включая тот, который вызывает собственный setWidth; оно останавливается только потому, что ширина больше не меняется.

Предпочитайте useEffect

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

  • Эффект читает макет (размер, позицию, прокрутку).
  • Он меняет то, что рендерится, на основе этого чтения.
  • Промежуточное состояние было бы видно и выглядело бы неправильно.

Всё остальное (загрузка данных, подписки, таймеры, логирование, аналитика) место в useEffect. Простое правило: начинайте с useEffect и переходите, только если видите мерцание.

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

Серверный рендеринг

Эффекты никогда не выполняются во время серверного рендеринга, и useLayoutEffect тоже. Сервер отправляет HTML первого рендера, а layout-эффект выполняется только после гидратации в браузере. React 18 и более ранние версии предупреждали "useLayoutEffect does nothing on the server", когда такой компонент рендерился на сервере; React 19 убрал это предупреждение.

Если первый кадр с сервера без измерения выглядел бы неправильно, есть два распространённых решения: рендерить нейтральную заглушку (например, скрывать подсказку, пока она не измерена) или рендерить компонент только на клиенте:

function ClientOnly({ children }) {
    const [mounted, setMounted] = useState(false);
    useEffect(() => setMounted(true), []);
    return mounted ? children : null;
}

useInsertionEffect

Есть и третий хук эффектов, useInsertionEffect, который выполняется раньше любого layout-эффекта. Он существует для библиотек CSS-in-JS, которые генерируют теги <style> во время выполнения: вставка стилей в этот момент означает, что layout-эффект, который измеряет элемент, уже видит его стилизованным. Он не может обновлять состояние, а рефы ещё не присоединены, поэтому в коде приложения от него нет пользы. Если вы не пишете библиотеку стилей, он вам не понадобится.

Часто задаваемые вопросы

Чем useEffect отличается от useLayoutEffect?

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

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

Когда эффект читает макет (размер или позицию) и сразу меняет на его основе то, что рендерится, например располагает подсказку над кнопкой или под ней. Без него пользователь может на один кадр увидеть первую, неправильную позицию.

useLayoutEffect плох для производительности?

Он не даёт браузеру отрисовать страницу, пока не закончит работу, вместе с любым повторным рендером, который он вызывает. Медленный layout-эффект делает всю страницу медленной, поэтому используйте useEffect, если только не нужно избежать видимого мерцания.

Выполняется ли useLayoutEffect на сервере?

Нет. Ни один эффект не выполняется во время серверного рендеринга. Старые версии React выводили предупреждение, когда компонент с useLayoutEffect рендерился на сервере; React 19 больше этого не делает, но эффект по-прежнему выполняется только в браузере.

Иллюстрация языков программирования Coddy

Учитесь программировать с Coddy

НАЧАТЬ