Spec-Zone.ru › React 18

useLayoutEffect

Опасность

useLayoutEffect может навредить производительности. Предпочитайте useEffect, если это возможно.

useLayoutEffect — это версия useEffect, которая срабатывает до перерисовки экрана браузером.

useLayoutEffect(setup, dependencies?)
  • Справочник
    • useLayoutEffect(setup, dependencies?)
  • Использование
    • Измерение макета перед перерисовкой экрана браузером
  • Отладка
    • Ошибка: «useLayoutEffect не работает на сервере»

Справочник

useLayoutEffect(setup, dependencies?)

Вызовите useLayoutEffect для выполнения измерений макета перед перерисовкой экрана браузером:

import { useState, useRef, useLayoutEffect } from 'react';

function Tooltip() {
  const ref = useRef(null);
  const [tooltipHeight, setTooltipHeight] = useState(0);

  useLayoutEffect(() => {
    const { height } = ref.current.getBoundingClientRect();
    setTooltipHeight(height);
  }, []);
  // ...

См. примеры ниже.

Параметры

  • setup: Функция с логикой вашего эффекта. Ваша функция настройки также может (необязательно) возвращать функцию очистки. Перед добавлением компонента в DOM React выполнит вашу функцию настройки. После каждой перерисовки с изменёнными зависимостями React сначала выполнит функцию очистки (если вы её предоставили) со старыми значениями, а затем выполнит вашу функцию настройки с новыми значениями. Перед удалением компонента из DOM React выполнит вашу функцию очистки.

  • Необязательно dependencies: Список всех реактивных значений, на которые ссылается код setup. Реактивные значения включают props, состояние и все переменные и функции, объявленные непосредственно внутри тела вашего компонента. Если ваш линтер настроен для React, он проверит, что каждое реактивное значение правильно указано как зависимость. Список зависимостей должен иметь постоянное число элементов и быть записан в строку, как в примере [dep1, dep2, dep3]. React будет сравнивать каждую зависимость со своим предыдущим значением, используя сравнение Object.is. Если вы опустите этот аргумент, ваш эффект будет перевыполняться после каждой перерисовки компонента.

Возвращает

useLayoutEffect возвращает undefined.

Ограничения

  • useLayoutEffect — это хук, поэтому вы можете вызывать его только на верхнем уровне вашего компонента или ваших собственных хуков. Вы не можете вызывать его внутри циклов или условий. Если вам это нужно, извлеките компонент и переместите эффект туда.

  • Когда режим строгости включён, React выполнит один дополнительный цикл настройки + очистки для разработки перед первым реальным циклом настройки. Это тест, гарантирующий, что ваша логика очистки «отражает» логику настройки и что она останавливает или отменяет действия настройки. Если это вызывает проблему, реализуйте функцию очистки.

  • Если некоторые из ваших зависимостей являются объектами или функциями, определёнными внутри компонента, существует риск, что они вызовут перевыполнение эффекта чаще, чем необходимо. Чтобы исправить это, удалите ненужные зависимости объектов и функций. Вы также можете извлечь обновления состояния и нереактивную логику за пределами вашего эффекта.

  • Эффекты выполняются только на клиенте. Они не выполняются во время серверной отрисовки.

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

  • Если вы вызываете обновление состояния внутри useLayoutEffect, React немедленно выполнит все оставшиеся эффекты, включая useEffect.


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

Измерение макета перед перерисовкой экрана браузером

Большинству компонентов не нужно знать своё положение и размер на экране, чтобы решить, что отобразить. Они просто возвращают JSX. Затем браузер вычисляет свой макет (положение и размер) и перерисовывает экран.

Иногда этого недостаточно. Представьте всплывающую подсказку, которая появляется рядом с элементом при наведении курсора. Если есть достаточно места, подсказка должна появиться над элементом, но если её не поместить, она должна появиться ниже. Для отображения подсказки в правильном окончательном положении нужно знать её высоту (т. е. поместится ли она сверху).

Для этого нужно выполнить рендеринг в два этапа:

  1. Отобразите подсказку где угодно (даже с неправильным положением).
  2. Измерьте её высоту и определите, где разместить подсказку.
  3. Отобразите подсказку снова в правильном месте.

Всё это должно произойти до перерисовки экрана браузером. Вы не хотите, чтобы пользователь видел, как перемещается подсказка. Используйте useLayoutEffect для выполнения измерений макета перед перерисовкой экрана браузером:

function Tooltip() {
  const ref = useRef(null);
  const [tooltipHeight, setTooltipHeight] = useState(0); // You don't know real height yet

  useLayoutEffect(() => {
    const { height } = ref.current.getBoundingClientRect();
    setTooltipHeight(height); // Re-render now that you know the real height
  }, []);

  // ...use tooltipHeight in the rendering logic below...
}

Вот как это работает пошагово:

  1. Tooltip отображается с начальным tooltipHeight = 0 (поэтому подсказка может быть неправильно расположена).
  2. React помещает её в DOM и выполняет код в useLayoutEffect.
  3. Ваш useLayoutEffect измеряет высоту содержимого подсказки и вызывает немедленную перерисовку.
  4. Tooltip снова отображается с реальным tooltipHeight (поэтому подсказка расположена правильно).
  5. React обновляет её в DOM, и браузер, наконец, отображает подсказку.

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

import { useRef, useLayoutEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import TooltipContainer from './TooltipContainer.js';

export default function Tooltip({ children, targetRect }) {
  const ref = useRef(null);
  const [tooltipHeight, setTooltipHeight] = useState(0);

  useLayoutEffect(() => {
    const { height } = ref.current.getBoundingClientRect();
    setTooltipHeight(height);
    console.log('Measured tooltip height: ' + height);
  }, []);

  let tooltipX = 0;
  let tooltipY = 0;
  if (targetRect !== null) {
    tooltipX = targetRect.left;
    tooltipY = targetRect.top - tooltipHeight;
    if (tooltipY < 0) {
      // It doesn't fit above, so place below.
      tooltipY = targetRect.bottom;
    }
  }

  return createPortal(
    <TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}>
      {children}
    </TooltipContainer>,
    document.body
  );
}

Заметьте, что даже если компоненту Tooltip необходимо отобразить два раза (сначала с tooltipHeight, установленным на 0, а затем с реальной измеренной высотой), вы видите только конечный результат. Вот почему для этого примера нужен useLayoutEffect вместо useEffect. Давайте рассмотрим разницу подробнее ниже.

useLayoutEffect vs useEffect

Пример 1 из 2:
useLayoutEffect блокирует браузер от перерисовки

React гарантирует, что код внутри useLayoutEffect и любые обновления состояния, запланированные внутри него, будут обработаны до перерисовки экрана браузером. Это позволяет отобразить подсказку, измерить её и снова отобразить подсказку без того, чтобы пользователь заметил первую дополнительную перерисовку. Другими словами, useLayoutEffect блокирует браузер от перерисовки.

import { useRef, useLayoutEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import TooltipContainer from './TooltipContainer.js';

export default function Tooltip({ children, targetRect }) {
  const ref = useRef(null);
  const [tooltipHeight, setTooltipHeight] = useState(0);

  useLayoutEffect(() => {
    const { height } = ref.current.getBoundingClientRect();
    setTooltipHeight(height);
  }, []);

  let tooltipX = 0;
  let tooltipY = 0;
  if (targetRect !== null) {
    tooltipX = targetRect.left;
    tooltipY = targetRect.top - tooltipHeight;
    if (tooltipY < 0) {
      // It doesn't fit above, so place below.
      tooltipY = targetRect.bottom;
    }
  }

  return createPortal(
    <TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}>
      {children}
    </TooltipContainer>,
    document.body
  );
}

Примечание

Отрисовка в два этапа и блокировка браузера ухудшают производительность. Старайтесь избегать этого, когда это возможно.


Отладка

Ошибка: «useLayoutEffect не работает на сервере»

useLayoutEffect призван позволить вашему компоненту использовать информацию о макете для рендеринга:

  1. Отобразить начальное содержимое.
  2. Измерить макет перед перерисовкой экрана браузером.
  3. Отобразить окончательное содержимое с использованием полученной информации о макете.

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

Проблема в том, что на сервере нет информации о макете.

В предыдущем примере вызов useLayoutEffect в компоненте Tooltip позволяет ему правильно позиционировать себя (либо над, либо под содержимым) в зависимости от высоты содержимого. Если бы вы попытались отобразить Tooltip как часть начального HTML сервера, это было бы невозможно определить. На сервере ещё нет макета! Поэтому, даже если бы вы отобразили его на сервере, его положение «подпрыгнуло» бы на клиенте после загрузки и выполнения JavaScript.

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

Однако, если у вас возникла эта проблема, у вас есть несколько вариантов:

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

  • В качестве альтернативы, пометьте свой компонент как клиентский. Это говорит React заменить его содержимое до ближайшей <Suspense> границы заглушкой (например, спиннером или мерцанием) во время серверной отрисовки.

  • В качестве альтернативы можно отобразить компонент с useLayoutEffect только после гидратации. Сохраните булевое isMounted состояние, инициализированное false, и установите его в true внутри вызова useEffect. Ваша логика рендеринга может быть, как в примере return isMounted ? <RealContent /> : <FallbackContent />. На сервере и во время гидратации пользователь увидит FallbackContent, которая не должна вызывать useLayoutEffect. Затем React заменит её на RealContent, которая выполняется только на клиенте и может включать в себя вызовы useLayoutEffect.

  • Если вы синхронизируете свой компонент с внешним хранилищем данных и полагаетесь на useLayoutEffect по причинам, отличным от измерения макета, рассмотрите useSyncExternalStore, который поддерживает серверную отрисовку.

© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://react.dev/reference/react/useLayoutEffect

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API