useLayoutEffect
useLayoutEffect
– это версия useEffect
, которая срабатывает перед тем, как браузер перерисует экран.
useLayoutEffect(setup, dependencies?)
Справочник
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
: Функция с логикой вашего эффекта. Ваша setup-функция, опционально, может возвращать функцию очистки. Перед тем, как ваш компонент добавится в DOM, React запустит вашу setup-функцию. После каждого повторного рендера с изменёнными зависимостями, React запустит функцию очистки (если вы её предоставили) со старыми значениями, а затем запустит вашу setup-функцию с новыми значениями. Перед тем как ваш компонент удалится из DOM, React запустит функцию очистки. -
dependencies
: Список всех реактивных значений, на которые ссылается код функцииsetup
. К реактивным значениям относятся пропсы, состояние, а также все переменные и функции, объявленные непосредственно в теле компонента. Если ваш линтер настроен для использования с React, он проверит, что каждое реактивное значение правильно указано как зависимость. Список зависимостей должен иметь постоянное количество элементов и быть записан примерно так:[dep1, dep2, dep3]
. React будет сравнивать каждую зависимость с предыдущим значением, используя алгоритм сравненияObject.is
. Если не указать зависимости вообще, то эффект будет запускаться заново после каждого повторного рендера компонента.
Возвращаемое значение
useLayoutEffect
возвращает undefined
.
Предостережения
-
useLayoutEffect
— это хук, поэтому вы можете вызывать его только на верхнем уровне вашего компонента или собственных хуков. Вы не можете вызывать его внутри циклов или условий. Если вам это нужно, выделите компонент и перенесите эффект туда. -
Когда включен строгий режим (Strict Mode), React выполнит один дополнительный цикл инициализации и сброса предназначенный только для разработки, перед первой реальной инициализаций. Это стресс-тест, который гарантирует, что ваша логика сброса “зеркально отражает” вашу логику инициализации и что она останавливает или отменяет все, что делает инициализация. Если это вызывает проблему, реализуйте функцию сброса.
-
Если некоторые из ваших зависимостей являются объектами или функциями, определенными внутри компонента, существует риск, что они будут вызывать повторное выполнение эффекта чаще, чем необходимо. Чтобы исправить это, удалите ненужные зависимости от объектов и функций. Вы также можете вынести обновления состояния и не реактивную логику за пределы вашего эффекта.
-
Эффекты выполняются только на клиенте. Они не выполняются во время серверного рендеринга.
-
Код внутри
useLayoutEffect
и все обновления состояния, запланированные из него, блокируют браузер от перерисовки экрана. При чрезмерном использовании это замедляет работу вашего приложения. По возможности предпочитайтеuseEffect
.
Использование
Измерение макета перед тем, как браузер перерисует экран.
Большинству компонентов не нужно знать их положение и размер на экране, чтобы решить, что рендерить. Они просто возвращают некоторый JSX. Затем браузер рассчитывает их макет (положение и размер) и перерисовывает экран.
Иногда этого недостаточно. Представьте себе всплывающую подсказку, которая появляется рядом с каким-то элементом при наведении. Если достаточно места, подсказка должна появиться над элементом, но если она не помещается, она должна появиться ниже. Чтобы отобразить всплывающую подсказку в правильной конечной позиции, вам нужно знать ее высоту (т.е. помещается ли она сверху).
Чтобы сделать это, нужно выполнить рендеринг в два этапа:
- Отрендерить всплывающую подсказку в любом месте (даже с неправильной позицией).
- Измерить ее высоту и решить, где разместить подсказку.
- Отрендерить всплывающую подсказку снова в правильном месте.
Все это должно произойти до того, как браузер перерисует экран. Вы не хотите, чтобы пользователь видел перемещение всплывающей подсказки. Вызовите useLayoutEffect
, чтобы выполнить измерения макета перед тем, как браузер перерисует экран:
function Tooltip() {
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0); // Вы еще не знаете реальную высоту
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height); // Теперь вызовите повторный рендер, когда вы знаете реальную высоту.
}, []);
// ...используйте tooltipHeight в логике рендеринга ниже...
}
Вот как это работает шаг за шагом:
Tooltip
рендерится с начальными значениемtooltipHeight = 0
(поэтому подсказка может быть неправильно расположена).- React помещает её в DOM и выполняет код в
useLayoutEffect
. - Ваш
useLayoutEffect
измеряет высоту содержимого всплывающей подсказки и инициирует немедленный повторный рендеринг. Tooltip
снова рендерится с реальнойtooltipHeight
(так что подсказка правильно расположена).- 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('Измеренная высота всплывающей подсказки: ' + height); }, []); let tooltipX = 0; let tooltipY = 0; if (targetRect !== null) { tooltipX = targetRect.left; tooltipY = targetRect.top - tooltipHeight; if (tooltipY < 0) { // Она не помещается сверху, поэтому размещаем снизу. tooltipY = targetRect.bottom; } } return createPortal( <TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}> {children} </TooltipContainer>, document.body ); }
Обратите внимание, что даже несмотря на то, что компонент Tooltip
должен рендериться в два этапа (сначала с tooltipHeight
, инициализированным на 0
, а затем с реальной измеренной высотой), вы видите только конечный результат. Вот почему для этого примера вам нужен useLayoutEffect
, а не useEffect
. Давайте подробно рассмотрим разницу ниже.
Example 1 of 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) { // Она не помещается сверху, поэтому размещаем снизу. tooltipY = targetRect.bottom; } } return createPortal( <TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}> {children} </TooltipContainer>, document.body ); }
Устранение неполадок
Я получаю ошибку: “useLayoutEffect
ничего не делает на сервере”
Цель useLayoutEffect
заключается в том, чтобы позволить вашему компоненту использовать информацию о макете для рендеринга:
- Отрендерить начальное содержимое.
- Измерить макет перед тем, как браузер перерисует экран.
- Отрендерить конечное содержимое, используя считанную информацию о макете.
Когда вы или ваш фреймворк используете серверный рендеринг, ваше 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
. Этот хук поддерживает серверный рендеринг.