useLayoutEffect

易犯错误

useLayoutEffect 会影响性能。尽可能选择 useEffect

¥useLayoutEffect can hurt performance. Prefer useEffect when possible.

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

¥useLayoutEffect is a version of useEffect that fires before the browser repaints the screen.

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

请参阅下面的更多示例。

¥See more examples below.

参数

¥Parameters

  • setup:具有副作用逻辑的函数。你的设置函数也可以选择返回一个清理函数。在你的组件被添加到 DOM 之前,React 将运行你的设置函数。在每次使用更改的依赖重新渲染后,React 将首先使用旧值运行清理函数(如果你提供了它),然后使用新值运行你的设置函数。在你的组件从 DOM 中删除之前,React 将运行你的清理函数。

    ¥setup: The function with your Effect’s logic. Your setup function may also optionally return a cleanup function. Before your component is added to the DOM, React will run your setup function. After every re-render with changed dependencies, React will first run the cleanup function (if you provided it) with the old values, and then run your setup function with the new values. Before your component is removed from the DOM, React will run your cleanup function.

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

    ¥optional dependencies: The list of all reactive values referenced inside of the setup code. Reactive values include props, state, and all the variables and functions declared directly inside your component body. If your linter is configured for React, it will verify that every reactive value is correctly specified as a dependency. The list of dependencies must have a constant number of items and be written inline like [dep1, dep2, dep3]. React will compare each dependency with its previous value using the Object.is comparison. If you omit this argument, your Effect will re-run after every re-render of the component.

返回

¥Returns

useLayoutEffect 返回 undefined

¥useLayoutEffect returns undefined.

注意事项

¥Caveats

  • useLayoutEffect 是一个 Hook,所以你只能在你的组件的顶层或者你自己的钩子中调用它。你不能在循环或条件内调用它。如果需要,请提取一个组件并将副作用移到那里。

    ¥useLayoutEffect is a Hook, so you can only call it at the top level of your component or your own Hooks. You can’t call it inside loops or conditions. If you need that, extract a component and move the Effect there.

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

    ¥When Strict Mode is on, React will run one extra development-only setup+cleanup cycle before the first real setup. This is a stress-test that ensures that your cleanup logic “mirrors” your setup logic and that it stops or undoes whatever the setup is doing. If this causes a problem, implement the cleanup function.

  • 如果你的某些依赖是在组件内部定义的对象或函数,则存在它们会导致副作用重新运行频率超过所需频率的风险。要解决此问题,请删除不必要的 object函数 依赖。你也可以在副作用器之外进行 提取状态更新非 React 性逻辑

    ¥If some of your dependencies are objects or functions defined inside the component, there is a risk that they will cause the Effect to re-run more often than needed. To fix this, remove unnecessary object and function dependencies. You can also extract state updates and non-reactive logic outside of your Effect.

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

    ¥Effects only run on the client. They don’t run during server rendering.

  • useLayoutEffect 中的代码以及从中安排的所有状态更新都会阻止浏览器重新绘制屏幕。当过度使用时,这会使你的应用变慢。如果可能,更喜欢 useEffect

    ¥The code inside useLayoutEffect and all state updates scheduled from it block the browser from repainting the screen. When used excessively, this makes your app slow. When possible, prefer useEffect.

  • 如果你在 useLayoutEffect 中触发状态更新,React 将立即执行所有剩余的效果,包括 useEffect

    ¥If you trigger a state update inside useLayoutEffect, React will execute all remaining Effects immediately including 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. 在任何地方渲染工具提示(即使位置错误)。

    ¥Render the tooltip anywhere (even with a wrong position).

  2. 测量它的高度并决定放置工具提示的位置。

    ¥Measure its height and decide where to place the tooltip.

  3. 在正确的位置再次渲染工具提示。

    ¥Render the tooltip again in the correct place.

所有这些都需要在浏览器重新绘制屏幕之前发生。你不希望用户看到工具提示在移动。在浏览器重绘屏幕之前调用 useLayoutEffect 执行布局测量:

¥All of this needs to happen before the browser repaints the screen. You don’t want the user to see the tooltip moving. Call useLayoutEffect to perform the layout measurements before the browser repaints the screen:

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 进行渲染(因此工具提示可能位置错误)。

    ¥Tooltip renders with the initial tooltipHeight = 0 (so the tooltip may be wrongly positioned).

  2. React 将它放在 DOM 中并运行 useLayoutEffect 中的代码。

    ¥React places it in the DOM and runs the code in useLayoutEffect.

  3. 你的工具提示内容的 useLayoutEffect 测量高度 并触发立即重新渲染。

    ¥Your useLayoutEffect measures the height of the tooltip content and triggers an immediate re-render.

  4. Tooltip 使用真正的 tooltipHeight 再次渲染(因此工具提示正确定位)。

    ¥Tooltip renders again with the real tooltipHeight (so the tooltip is correctly positioned).

  5. React 在 DOM 中更新它,浏览器最终显示工具提示。

    ¥React updates it in the DOM, and the browser finally displays the tooltip.

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

¥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 / 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. 渲染初始内容。

    ¥Render the initial content.

  2. 在浏览器重新绘制屏幕之前测量布局。

    ¥Measure the layout before the browser repaints the screen.

  3. 使用你已阅读的布局信息渲染最终内容。

    ¥Render the final content using the layout information you’ve read.

当你或你的框架使用 服务器渲染 时,你的 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:

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

    ¥Replace useLayoutEffect with useEffect. This tells React that it’s okay to display the initial render result without blocking the paint (because the original HTML will become visible before your Effect runs).

  • 或者,将你的组件标记为仅限客户端。 This 告诉 React 在服务器渲染期间将其内容替换为最近的 <Suspense> 边界,并使用加载回退(例如,加载控件或闪烁)。

    ¥Alternatively, mark your component as client-only. This tells React to replace its content up to the closest <Suspense> boundary with a loading fallback (for example, a spinner or a glimmer) during server rendering.

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

    ¥Alternatively, you can render a component with useLayoutEffect only after hydration. Keep a boolean isMounted state that’s initialized to false, and set it to true inside a useEffect call. Your rendering logic can then be like return isMounted ? <RealContent /> : <FallbackContent />. On the server and during the hydration, the user will see FallbackContent which should not call useLayoutEffect. Then React will replace it with RealContent which runs on the client only and can include useLayoutEffect calls.

  • 如果你将你的组件与外部数据存储同步并出于与测量布局不同的原因依赖 useLayoutEffect,请考虑 useSyncExternalStore 而不是 支持服务器渲染。

    ¥If you synchronize your component with an external data store and rely on useLayoutEffect for different reasons than measuring layout, consider useSyncExternalStore instead which supports server rendering.


React 中文网 - 粤ICP备13048890号