useLayoutEffect

陷阱

useLayoutEffect 会影响性能。如果可能,优先使用 useEffect

useLayoutEffectuseEffect 的一个版本,它在浏览器重绘屏幕之前触发。

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:包含 Effect 逻辑的函数。您的 setup 函数还可以选择返回一个 *清理* 函数。在您的组件添加到 DOM 之前,React 将运行您的 setup 函数。每次重新渲染并更改依赖项后,React 都会首先使用旧值运行清理函数(如果提供),然后使用新值运行您的 setup 函数。在您的组件从 DOM 中移除之前,React 将运行您的清理函数。

  • 可选 dependenciessetup 代码中引用的所有反应式值的列表。反应式值包括 props、状态以及在组件主体中直接声明的所有变量和函数。如果您的代码检查器已为 React 配置,它将验证每个反应式值是否正确指定为依赖项。依赖项列表必须具有恒定的项目数,并像 [dep1, dep2, dep3] 一样内联编写。React 将使用 Object.is 比较将每个依赖项与其先前值进行比较。如果省略此参数,则您的 Effect 将在组件的每次重新渲染后重新运行。

返回值

useLayoutEffect 返回 undefined

注意事项

  • useLayoutEffect 是一个 Hook,因此你只能在组件的顶层或你自己的 Hook 中调用它。你不能在循环或条件语句内调用它。如果你需要这样做,请提取一个组件并将 Effect 移动到那里。

  • 当启用严格模式时,React 会在第一次真正的设置之前运行一个额外的仅限开发环境的设置+清理周期。这是一个压力测试,它确保你的清理逻辑“镜像”你的设置逻辑,并且它会停止或撤消设置正在执行的操作。如果这导致问题,请实现清理函数

  • 如果你的某些依赖项是在组件内部定义的对象或函数,则它们可能导致 Effect 比需要的更频繁地重新运行。要解决此问题,请删除不必要的对象函数依赖项。你还可以提取状态更新非反应式逻辑到你的 Effect 外部。

  • Effect 只在客户端运行。它们不会在服务器渲染期间运行。

  • useLayoutEffect 内部的代码以及从中调度的所有状态更新都会阻止浏览器重绘屏幕。过度使用时,这会使你的应用程序变慢。如果可能,请优先使用useEffect

  • 如果在 useLayoutEffect 内触发状态更新,React 将立即执行所有剩余的 Effect,包括 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 组件必须进行两遍渲染(首先,使用初始化为0tooltipHeight,然后使用实际测量的高度),你只看到最终结果。这就是为什么在这个例子中你需要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 以进行初始渲染。这允许您在 JavaScript 代码加载之前显示初始 HTML。

问题在于,在服务器上,没有布局信息。

前面的示例中,Tooltip 组件中的 useLayoutEffect 调用允许它根据内容高度正确地定位自身(在内容上方或下方)。如果您尝试将 Tooltip 作为初始服务器 HTML 的一部分进行渲染,这将无法确定。在服务器上,还没有布局!因此,即使您在服务器上渲染它,它的位置也会在 JavaScript 加载并运行后在客户端“跳跃”。

通常,依赖于布局信息的组件不需要在服务器上进行渲染。例如,在初始渲染期间显示 Tooltip 可能没有意义。它是由客户端交互触发的。

但是,如果您遇到此问题,您有几种不同的选择

  • useLayoutEffect 替换为 useEffect 这告诉 React 在不阻止绘制的情况下显示初始渲染结果是可以的(因为原始 HTML 将在您的 Effect 运行之前可见)。

  • 或者,将您的组件标记为仅限客户端。 这告诉 React 在服务器端渲染期间使用加载后备(例如,加载指示器或闪烁效果)替换其内容到最近的 <Suspense> 边界。

  • 或者,您可以在水合后才渲染包含 useLayoutEffect 的组件。保留一个布尔值 isMounted 状态,初始化为 false,并在 useEffect 调用中将其设置为 true。然后您的渲染逻辑可以像 return isMounted ? <RealContent /> : <FallbackContent /> 一样。在服务器端和水合期间,用户将看到 FallbackContent,它不应该调用 useLayoutEffect。然后 React 将用 RealContent 替换它,该内容仅在客户端运行,并且可以包含 useLayoutEffect 调用。

  • 如果您使用外部数据存储同步您的组件,并且依赖于 useLayoutEffect 的原因与测量布局不同,请考虑使用 useSyncExternalStore,它支持服务器端渲染。