useLayoutEffect

易犯错误

useLayoutEffect 可能会影响性能。尽可能优先使用 useEffect

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

useLayoutEffect(setup, dependencies?)

参考

🌐 Reference

useLayoutEffect(setup, dependencies?)

在浏览器重新绘制屏幕之前,调用 useLayoutEffect 来执行布局测量:

🌐 Call useLayoutEffect to perform the layout measurements before the browser repaints the screen:

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);
}, []);
// ...

查看更多示例。

参数

🌐 Parameters

  • setup:具有你 Effect 逻辑的函数。你的 setup 函数也可以选择性地返回一个 cleanup 函数。在你的 组件提交 之前,React 会运行你的 setup 函数。在每次提交且依赖发生变化后,React 会先使用旧值运行 cleanup 函数(如果你提供了它),然后使用新值运行你的 setup 函数。在你的组件从 DOM 中被移除之前,React 会运行你的 cleanup 函数。
  • 可选 dependenciessetup 代码内部引用的所有响应式值的列表。响应式值包括 props、state 以及直接在组件主体内声明的所有变量和函数。如果你的 linter 已经为 React 配置,它将验证每个响应式值是否正确指定为依赖。依赖列表必须具有固定数量的项目,并且像 [dep1, dep2, dep3] 一样内联编写。React 将使用 Object.is 比较每个依赖与其之前的值。如果你省略此参数,你的 Effect 将在组件每次提交后重新运行。

返回

🌐 Returns

useLayoutEffect 返回 undefined

注意事项

🌐 Caveats

  • useLayoutEffect 是一个 Hook,所以你只能在组件的顶层或你自己的 Hook 中调用它。你不能在循环或条件语句中调用它。如果你需要那样做,提取一个组件并把 Effect 移到那里。
  • 当严格模式开启时,React 会在第一次真正的设置之前运行一次额外的仅限开发的设置+清理周期。这是一个压力测试,用于确保你的清理逻辑“镜像”你的设置逻辑,并且停止或撤销设置正在执行的操作。如果这导致问题,请实现清理函数。
  • 如果你的一些依赖是组件内部定义的对象或函数,就有可能**导致 Effect 比预期更频繁地重新运行。**要解决这个问题,请移除不必要的对象函数依赖。你也可以将状态更新非响应式逻辑提取到 Effect 外部。
  • 效果仅在客户端运行。 它们在服务器渲染期间不会运行。
  • useLayoutEffect 内的代码以及由其调度的所有状态更新会阻止浏览器重绘屏幕。 当过度使用时,这会使你的应用变慢。尽可能情况下,优先使用 useEffect.
  • 如果你在 useLayoutEffect 内触发状态更新,React 将立即执行所有剩余的 Effect,包括 useEffect

用法

🌐 Usage

在浏览器重绘屏幕之前测量布局

🌐 Measuring layout before the browser repaints the screen

大多数组件不需要知道它们在屏幕上的位置和大小来决定要渲染什么。它们只返回一些 JSX。然后浏览器会计算它们的布局(位置和大小)并重新绘制屏幕。

🌐 Most components don’t need to know their position and size on the screen to decide what to render. They only return some JSX. Then the browser calculates their layout (position and size) and repaints the screen.

有时,这还不够。想象一个在悬停时出现在某个元素旁边的工具提示。如果有足够的空间,工具提示应该出现在元素的上方,但如果空间不足,它应该出现在下方。为了将工具提示渲染在正确的最终位置,你需要知道它的高度(即它是否适合放在顶部)。

🌐 Sometimes, that’s not enough. Imagine a tooltip that appears next to some element on hover. If there’s enough space, the tooltip should appear above the element, but if it doesn’t fit, it should appear below. In order to render the tooltip at the right final position, you need to know its height (i.e. whether it fits at the top).

为此,你需要分两次渲染:

🌐 To do this, you need to render in two passes:

  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...
}

这是一步一步的工作方式:

🌐 Here’s how this works step by step:

  1. Tooltip 会以初始的 tooltipHeight = 0 渲染(所以工具提示可能会位置错误)。
  2. React 将它放入 DOM 并运行 useLayoutEffect 中的代码。
  3. 你的 useLayoutEffect 测量工具提示内容的高度 并触发立即重新渲染。
  4. Tooltip 会再次使用真实的 tooltipHeight 渲染(这样工具提示就能正确定位)。
  5. React 在 DOM 中更新它,浏览器最终显示工具提示。

将鼠标悬停在下面的按钮上,查看工具提示如何根据是否适合来调整其位置:

🌐 Hover over the buttons below and see how the tooltip adjusts its position depending on whether it fits:

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 的原因。让我们在下面详细看看差异。

🌐 Notice that even though the Tooltip component has to render in two passes (first, with tooltipHeight initialized to 0 and then with the real measured height), you only see the final result. This is why you need useLayoutEffect instead of useEffect for this example. Let’s look at the difference in detail below.

useLayoutEffect vs useEffect

例子 1 of 2:
useLayoutEffect 阻止浏览器重新渲染

🌐 useLayoutEffect blocks the browser from repainting

React 保证 useLayoutEffect 内的代码以及其中调度的任何状态更新都会在 浏览器重绘屏幕之前 处理完毕。这使你可以渲染工具提示、测量它,然后再次重新渲染工具提示,而用户不会注意到第一次额外的渲染。换句话说,useLayoutEffect 会阻止浏览器进行绘制。

🌐 React guarantees that the code inside useLayoutEffect and any state updates scheduled inside it will be processed before the browser repaints the screen. This lets you render the tooltip, measure it, and re-render the tooltip again without the user noticing the first extra render. In other words, useLayoutEffect blocks the browser from painting.

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
  );
}

注意

分两次渲染并阻塞浏览器会影响性能。尽量在可能的情况下避免这样做。

🌐 Rendering in two passes and blocking the browser hurts performance. Try to avoid this when you can.


故障排除

🌐 Troubleshooting

我收到一个错误:“useLayoutEffect 在服务器上没有任何作用”

🌐 I’m getting an error: “useLayoutEffect does nothing on the server”

useLayoutEffect 的目的是让你的组件 使用布局信息进行渲染:

🌐 The purpose of useLayoutEffect is to let your component use layout information for rendering:

  1. 渲染初始内容。
  2. 在浏览器重新绘制屏幕之前测量布局。
  3. 使用你已阅读的布局信息渲染最终内容。

当你或你的框架使用服务器渲染时,你的 React 应用会在服务器上将初始渲染的内容呈现为 HTML。这使你可以在 JavaScript 代码加载之前显示初始的 HTML。

🌐 When you or your framework uses server rendering, your React app renders to HTML on the server for the initial render. This lets you show the initial HTML before the JavaScript code loads.

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

🌐 The problem is that on the server, there is no layout information.

前面的例子中,Tooltip组件中的useLayoutEffect调用使它能够根据内容高度正确定位自己(在内容上方或下方)。如果你尝试将Tooltip渲染为初始服务器 HTML 的一部分,这是无法确定的。在服务器上,还没有布局!所以,即使在服务器上渲染它,它的位置在 JavaScript 加载并运行后,在客户端仍会“跳动”。

🌐 In the earlier example, the useLayoutEffect call in the Tooltip component lets it position itself correctly (either above or below content) depending on the content height. If you tried to render Tooltip as a part of the initial server HTML, this would be impossible to determine. On the server, there is no layout yet! So, even if you rendered it on the server, its position would “jump” on the client after the JavaScript loads and runs.

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

🌐 Usually, components that rely on layout information don’t need to render on the server anyway. For example, it probably doesn’t make sense to show a Tooltip during the initial render. It is triggered by a client interaction.

但是,如果你遇到这个问题,你有几个不同的选择:

🌐 However, if you’re running into this problem, you have a few different options:

  • useLayoutEffect 替换为 useEffect. 这告诉 React 可以在不阻塞渲染的情况下显示初始渲染结果(因为原始 HTML 会在你的 Effect 运行之前变得可见)。
  • 或者,将你的组件标记为仅客户端。 这会告诉 React 在服务器渲染期间,将其内容替换到最近的 <Suspense> 边界为止,并显示一个加载回退(例如,一个加载旋转图标或闪烁效果)。
  • 或者,你可以仅在水合后使用 useLayoutEffect 渲染组件。保持一个布尔值的 isMounted 状态,该状态初始化为 false,并在 useEffect 调用中将其设置为 true。你的渲染逻辑可以像 return isMounted ? <RealContent /> : <FallbackContent /> 那样。 在服务器和水合过程中,用户将看到 FallbackContent,这不应调用 useLayoutEffect。然后 React 会将其替换为 RealContent,它只在客户端运行,并且可以包含 useLayoutEffect 调用。
  • 如果你将组件与外部数据存储同步,并且出于与测量布局不同的原因依赖 useLayoutEffect,请考虑使用 useSyncExternalStore,它 支持服务器渲染