useLayoutEffect 是 useEffect 的一个版本,它在浏览器重绘屏幕之前触发。
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 函数。- 可选
dependencies:setup代码内部引用的所有响应式值的列表。响应式值包括 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:
- 在任何地方渲染工具提示(即使位置错误)。
- 测量它的高度并决定放置工具提示的位置。
- 在正确的位置再次显示工具提示。
所有这些都需要在浏览器重新绘制屏幕之前完成。 你不希望用户看到工具提示在移动。在浏览器重新绘制屏幕之前,调用 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:
Tooltip会以初始的tooltipHeight = 0渲染(所以工具提示可能会位置错误)。- React 将它放入 DOM 并运行
useLayoutEffect中的代码。 - 你的
useLayoutEffect测量工具提示内容的高度 并触发立即重新渲染。 Tooltip会再次使用真实的tooltipHeight渲染(这样工具提示就能正确定位)。- 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.
例子 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 ); }
故障排除
🌐 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:
- 渲染初始内容。
- 在浏览器重新绘制屏幕之前测量布局。
- 使用你已阅读的布局信息渲染最终内容。
当你或你的框架使用服务器渲染时,你的 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,它 支持服务器渲染。