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、状态以及直接在组件主体中声明的所有变量和函数。如果您的 linter 是为 React 配置的,它将验证每个响应式值是否都被正确指定为依赖项。依赖项列表必须具有恒定的项目数,并且像 [dep1, dep2, dep3] 一样内联编写。React 将使用 Object.is 比较将每个依赖项与其先前的值进行比较。如果您省略此参数,则每次组件重新渲染后,您的 Effect 都会重新运行。

返回值

useLayoutEffect 返回 undefined

注意事项

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

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

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

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

  • useLayoutEffect 内部代码以及从中安排的所有状态更新都会 阻止浏览器重新绘制屏幕。 如果过度使用,这会使你的应用程序变慢。如果可能,请优先使用 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 与 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 可能没有意义。它是由客户端交互触发的。

但是,如果您遇到此问题,您可以选择以下几种不同的方法

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

  • 或者,将您的组件标记为仅限客户端。这告诉 React 在服务器端渲染期间用加载回退(例如,微调器或微光)替换其内容,直到最近的 <Suspense> 边界。

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

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