useLayoutEffect 是 useEffect 的一个版本,它会在浏览器重绘屏幕之前触发。
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 函数也可以选择返回一个 清理 函数。在你的 组件提交 之前,React 会运行你的 setup 函数。每次依赖项发生变化并提交后,React 会先使用旧值运行清理函数(如果你提供了它),然后再使用新值运行你的 setup 函数。在你的组件从 DOM 中移除之前,React 会运行你的清理函数。 -
可选
dependencies:在setup代码中引用的所有响应式值的列表。响应式值包括 props、state,以及所有直接在组件函数体内声明的变量和函数。如果你的 linter 已为 React 配置,它会验证每个响应式值都被正确指定为依赖项。依赖项列表必须具有固定数量的项目,并且像[dep1, dep2, dep3]这样内联编写。React 会使用Object.is比较来将每个依赖项与其上一次的值进行比较。如果你省略此参数,你的 Effect 将在组件的每次提交后重新运行。
返回值
useLayoutEffect 返回 undefined。
注意事项
-
useLayoutEffect是一个 Hook,所以你只能在组件顶层或你自己的 Hooks 中调用它。你不能在循环或条件语句中调用它。如果你需要这样做,请提取一个组件并把 Effect 移到那里。 -
当严格模式开启时,React 会在第一次真实 setup 之前额外运行一次仅限开发环境的 setup+cleanup 循环。这是一个压力测试,用来确保你的清理逻辑与 setup 逻辑“镜像对应”,并且能停止或撤销 setup 所做的一切。如果这导致问题,请实现清理函数。
-
如果你的某些依赖项是在组件内部定义的对象或函数,那么它们有可能会**导致 Effect 比需要的更频繁地重新运行。**要修复这个问题,请移除不必要的 对象 和 函数 依赖项。你也可以将 基于前一个 state 的更新 和 非响应式逻辑 提取到 Effect 之外。
-
Effect **只会在客户端运行。**它们不会在服务器渲染期间运行。
-
useLayoutEffect内部的代码以及从它触发的所有 state 更新都会**阻止浏览器重绘屏幕。**过度使用会让你的应用变慢。尽可能优先使用useEffect. -
如果你在
useLayoutEffect内触发 state 更新,React 将会立即执行所有剩余的 Effects,包括useEffect。
用法
在浏览器重绘屏幕之前测量布局
大多数组件不需要知道它们在屏幕上的位置和大小来决定渲染什么。它们只需要返回一些 JSX。然后浏览器会计算它们的 布局(位置和大小)并重绘屏幕。
有时候,这还不够。想象一个悬浮提示在鼠标悬停时出现在某个元素旁边。如果空间足够,提示应显示在元素上方;但如果放不下,就应显示在下方。为了把提示渲染到正确的最终位置,你需要知道它的高度(即判断它能否放在上方)。
为此,你需要分两次渲染:
- 先在任意位置渲染提示(即使位置不正确)。
- 测量它的高度并决定把提示放在哪里。
- 再次以正确的位置渲染提示。
所有这些都需要在浏览器重绘屏幕之前完成。 你不希望用户看到提示在移动。调用 useLayoutEffect 可以在浏览器重绘屏幕之前执行布局测量:
function Tooltip() {
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0); // 你还不知道真实高度
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height); // 现在你知道真实高度了,重新渲染
}, []);
// ...在下面的渲染逻辑中使用 tooltipHeight...
}下面是它的工作过程,逐步来看:
Tooltip以初始的tooltipHeight = 0进行渲染(因此提示可能位置错误)。- React 将它放入 DOM,并运行
useLayoutEffect中的代码。 - 你的
useLayoutEffect测量提示内容的高度,并立即触发重新渲染。 Tooltip再次使用真实的tooltipHeight进行渲染(因此提示位置正确)。- 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('测量到的提示高度:' + height); }, []); let tooltipX = 0; let tooltipY = 0; if (targetRect !== null) { tooltipX = targetRect.left; tooltipY = targetRect.top - tooltipHeight; if (tooltipY < 0) { // 上方放不下,所以放在下方。 tooltipY = targetRect.bottom; } } return createPortal( <TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}> {children} </TooltipContainer>, document.body ); }
注意,尽管 Tooltip 组件必须分两次渲染(第一次 tooltipHeight 初始化为 0,然后再使用实际测得的高度渲染),你只会看到最终结果。这就是为什么在这个示例中你需要使用 useLayoutEffect 而不是 useEffect。下面我们将详细看看它们之间的区别。
Example 1 of 2: useLayoutEffect 会阻止浏览器重绘
React 保证 useLayoutEffect 内部的代码以及其中安排的任何 state 更新都会在浏览器重绘屏幕之前被处理。这样你就可以渲染提示、测量它,然后再次重新渲染提示,而用户不会注意到第一次额外渲染。换句话说,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) { // 上方放不下,所以放在下方。 tooltipY = targetRect.bottom; } } return createPortal( <TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}> {children} </TooltipContainer>, document.body ); }
故障排除
我遇到一个错误:“useLayoutEffect 在服务器上不起作用”
useLayoutEffect 的目的,是让你的组件在渲染时使用布局信息:
- 渲染初始内容。
- 在浏览器重绘屏幕之前测量布局。
- 使用你读取到的布局信息渲染最终内容。
当你或你的框架使用服务端渲染时,你的 React 应用会在首次渲染时于服务器上输出为 HTML。这样你就可以在 JavaScript 代码加载之前先显示初始 HTML。
问题在于,服务器上没有布局信息。
在前面的示例中,Tooltip 组件里的 useLayoutEffect 调用让它能够根据内容高度正确地定位自己(在内容上方或下方)。如果你尝试将 Tooltip 作为初始服务器 HTML 的一部分进行渲染,那么这是无法确定的。服务器上还没有布局!因此,即使你在服务器上渲染了它,等 JavaScript 加载并运行后,它的位置也会在客户端“跳动”一下。
通常,依赖布局信息的组件本来也不需要在服务器上渲染。例如,在初始渲染时显示一个 Tooltip 可能并没有意义。它是由客户端交互触发的。
不过,如果你遇到了这个问题,可以考虑以下几种不同的方案:
-
将
useLayoutEffect替换为useEffect。 这会告诉 React,可以在不阻塞绘制的情况下显示初始渲染结果(因为原始 HTML 会在你的 Effect 运行之前先变为可见)。 -
另外一种方式是,将你的组件标记为仅客户端渲染。 这会告诉 React:在服务端渲染期间,用加载占位内容替换其内容,直到最近的
<Suspense>边界为止(例如,加载指示器或闪烁占位)。 -
另外,你也可以只在 hydration 之后渲染包含
useLayoutEffect的组件。保留一个布尔值isMounted状态,初始值为false,然后在useEffect调用中将其设为true。你的渲染逻辑可以写成return isMounted ? <RealContent /> : <FallbackContent />。在服务器上以及 hydration 过程中,用户会看到FallbackContent,它不应该调用useLayoutEffect。随后 React 会将其替换为仅在客户端运行、并且可以包含useLayoutEffect调用的RealContent。 -
如果你的组件是与外部数据存储同步,并且依赖
useLayoutEffect的原因并非测量布局,那么可以考虑改用useSyncExternalStore,它支持服务端渲染。