A React hook that allows you to use a ResizeObserver to measure an element's size.
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 1 个月前 | ||
| 1 个月前 | ||
| 6 年前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 7 年前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 6 年前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 |
use-resize-observer

一个 React Hook,让你能够使用 ResizeObserver 来测量元素的尺寸。
从 v9 升级? v10 包含多项不兼容更新,请参阅 MIGRATION.md。
亮点
- 使用 TypeScript 编写。
- 零运行时依赖。
- 体积小巧:小于 1kB(已压缩并 gzip 处理),由 size-limit 监控(预算配置)。
- 提供 ESM 和 CJS 构建版本。
- 若需要更多控制,可使用 onResize 回调,该回调会接收原始的
ResizeObserverEntry以及测量到的尺寸。 box选项。- 支持 SSR。
- 支持 CSS-in-JS。
- 支持自定义 ref,以防你已经有一个 ref 了。
- 默认使用 RefCallback,以解决延迟挂载和 ref 元素变更的问题。
- 处理许多你可能未曾考虑到的边缘情况。 (参见本文档和测试用例。)
- 易于组合(节流/防抖、断点、元素坐标)
- 在真实浏览器中测试(最新版 Chrome、Firefox、Edge 和 Safari,以及真实的 iOS 和 Android 设备,由 BrowserStack 赞助)
要求
- React 18.2 或更新版本(同伴依赖范围是开放的,因此未来的主要版本也适用)。
- 一个
ResizeObserver实现。它在所有现代浏览器中都可用。要支持没有该实现的环境,您可以自行进行 polyfill,或者完全跳过观察。
实际效果
安装
pnpm add use-resize-observer
# or
npm install use-resize-observer
# or
yarn add use-resize-observer
选项
| 选项 | 类型 | 描述 | 默认值 |
|---|---|---|---|
| ref | undefined | RefObject | Element | 要观察的 ref 或元素。如果省略,则改用钩子返回的 ref 回调(推荐)。 | undefined |
| box | undefined | "border-box" | "content-box" | "device-pixel-content-box" | 用于观察的 盒模型。 | "content-box" |
| onResize | undefined | ({ width, height, entry }: ResizeHandlerPayload) => void | 接收元素尺寸和 原始 entry 的回调函数。提供此回调将选择仅回调模式:钩子不再返回/更新 width 和 height,也不再重新渲染 —— 您将负责更新(以及渲染优化)。 |
undefined |
| round | undefined | (n: number) => number | 用于舍入值的自定义函数,替代默认函数。 | Math.round() |
响应
| 名称 | 类型 | 描述 |
|---|---|---|
| ref | RefCallback | 要传递给 React 的 "ref" 属性的回调函数。 |
| width | undefined | number | 元素的宽度(或 "inlineSize")。 |
| height | undefined | number | 元素的高度(或 "blockSize")。 |
基本用法
useResizeObserver 是一个具名导出:
import { useResizeObserver } from "use-resize-observer";
const App = () => {
const { ref, width = 1, height = 1 } = useResizeObserver<HTMLDivElement>();
return (
<div ref={ref}>
Size: {width}x{height}
</div>
);
};
若要观察内容盒以外的其他盒尺寸,请传入 box 选项,如下所示:
const { ref, width, height } = useResizeObserver<HTMLDivElement>({
box: "border-box",
});
请注意,如果浏览器不支持指定的盒子类型,那么该 hook 也不会报告任何尺寸。
盒子选项
请注意,盒子选项是实验性的,因此并非所有实现了 ResizeObserver 的浏览器都支持它们。(参见此处。)
content-box(默认值)
所有实现了 ResizeObserver 的浏览器都可安全使用。如果 contentBoxSize 不可用,hook 内部将回退到旧规范中的 contentRect。
border-box
大多数现代浏览器都能很好地支持。但是,如果您需要支持这些浏览器的旧版本,则可能需要通过特性检测来确认是否支持。
device-pixel-content-box
Surma 写过一篇非常好的文章,介绍了如何使用此选项进行像素级完美渲染。但在撰写本文时,它的支持度有限(值得注意的是,Safari 不支持它)。在依赖它之前请进行特性检测。
自定义取整
默认情况下,此 hook 会将测量值通过 Math.round() 处理,以避免因亚像素变化而导致的重新渲染。
如果这不是您想要的行为,您可以提供自己的函数:
向下取整报告的值
const { ref, width, height } = useResizeObserver<HTMLDivElement>({
round: Math.floor,
});
跳过取整
import { useResizeObserver } from "use-resize-observer";
// Outside the hook to ensure this instance does not change unnecessarily.
const noop = (n: number) => n;
const App = () => {
const {
ref,
width = 1,
height = 1,
} = useResizeObserver<HTMLDivElement>({ round: noop });
return (
<div ref={ref}>
Size: {width}x{height}
</div>
);
};
请注意,round 选项对函数引用较为敏感,因此如果你的舍入函数不依赖任何钩子状态,请确保使用 useCallback或将其声明在钩子函数作用域之外。(如上所示。)
从默认 RefCallback 获取原始元素
请注意,上述示例中的“ref”是一个 RefCallback,而非 RefObject,这意味着如果你需要元素本身,将无法访问“ref.current”。
要获取原始元素,你可以使用自己的 RefObject(参见本文档后面的内容),或者将返回的 ref 与你自己的 ref 合并:
import { useResizeObserver } from "use-resize-observer";
import { mergeRefs } from "react-merge-refs";
const App = () => {
const { ref, width = 1, height = 1 } = useResizeObserver<HTMLDivElement>();
const mergedCallbackRef = mergeRefs([
ref,
(element: HTMLDivElement) => {
// Do whatever you want with the `element`.
},
]);
return (
<div ref={mergedCallbackRef}>
Size: {width}x{height}
</div>
);
};
传入自定义 ref
在可能的情况下,建议优先使用钩子返回的 RefCallback(如上述默认用法所示)—— 它能处理延迟挂载和随时间变化的元素。传入自定义 ref 的场景是当你已经有一个需要测量的 ref 时。
const ref = useRef<HTMLDivElement>(null);
const { width, height } = useResizeObserver<HTMLDivElement>({ ref });
你甚至可以复用同一个 hook 实例来测量不同的元素:
测量原始元素
在某些情况下,你可能已经有一个需要测量的元素。
ref 选项也接受原始元素,而不仅仅是 ref,因此你可以这样做:
const { width, height } = useResizeObserver<HTMLDivElement>({
ref: divElement,
});
来自其他窗口的元素(例如跨文档 iframe)也受支持。
使用单个 Hook 测量多个 Refs
该 hook 会对 ref 变化做出反应,因为它会将 ref 解析为要观察的元素。
这意味着你可以自由地将自定义 ref 选项从一个 ref 更改为另一个,然后再改回来,hook 会开始观察其选项中设置的任何内容。
选择不实例化(或延迟实例化)ResizeObserver
在某些情况下,你可能希望延迟创建 ResizeObserver 实例。
你可能会提供一个库,该库仅根据 props 有条件地提供观察功能,这意味着虽然你的组件中包含该 hook,但你可能不希望实际初始化它。
另一个例子是,当你运行某些测试时,环境可能不提供 ResizeObserver,这时你可能希望完全选择不初始化。
(参见相关讨论)
使用默认的 ref RefCallback,或仅在需要时有条件地提供自定义 ref。只有当有实际需要观察的内容时,hook 才会创建 ResizeObserver 实例。
“onResize” 回调函数
默认情况下,当目标元素的宽度和/或高度发生所有变化时,hook 都会触发重新渲染。
你可以通过提供 onResize 回调函数来选择退出此行为,当元素尺寸变化时,该函数会接收元素的宽度和高度,以便你可以决定如何处理这些信息:
import { useResizeObserver } from "use-resize-observer";
const App = () => {
// width / height will not be returned here when the onResize callback is present
const { ref } = useResizeObserver<HTMLDivElement>({
onResize: ({ width, height }) => {
// do something here.
},
});
return <div ref={ref} />;
};
原始 entry
除了已解析的 width / height 外,回调函数还会接收原始的 ResizeObserverEntry 作为 entry 参数。这为你提供了该 hook 本身未直接呈现的所有信息:
- 所有盒模型尺寸,无论使用的
box选项如何。浏览器会在每个条目中报告contentBoxSize、borderBoxSize和devicePixelContentBoxSize(在支持的情况下),因此你可以读取与正在观察的盒模型不同的尺寸。 - 被观察的元素,即
entry.target。当你使用返回的 ref 回调时,此属性非常有用,因为此时元素在回调函数中无法通过其他方式获取。它还允许你获取观察器根本不提供的信息,例如元素的 坐标。
const { ref } = useResizeObserver<HTMLDivElement>({
onResize: ({ width, height, entry }) => {
// e.g. the element itself, and the border box while observing the content box:
console.log(entry.target, entry.borderBoxSize);
},
});
此回调还能让你实现仅报告所需内容的自定义 Hook,例如:
- 仅报告宽度或高度
- 节流 / 防抖
- 用
requestAnimationFrame包装
Hook 组合
由于此 hook 旨在保持底层特性,因此如果需要额外功能,建议通过 hook 组合在其基础上进行构建。
节流 / 防抖
你可能希望接收值的频率低于实际变化发生的频率。
断点
另一个常见概念是断点。下面是一个实现此功能的简单 hook 示例。
元素坐标
该 hook 仅报告尺寸。如果你还需要元素的 x / y / top / left,可以使用 getBoundingClientRect 从元素中读取,原始 entry 为此提供了便捷的访问方式:
const { ref } = useResizeObserver({
onResize: ({ entry }) => {
requestAnimationFrame(() => {
const rect = entry.target.getBoundingClientRect();
// ... do something with the result ...
});
},
});
getBoundingClientRect() 会强制触发布局,因此这里将其包装在 requestAnimationFrame 中,以避免布局抖动。只有当你确实需要坐标时才使用此方法——如果你只需要宽度/高度,正常使用该 hook 即可,无需理会 entry。
默认值(SSR)
在初始挂载时,ResizeObserver 需要一点时间来报告实际尺寸。
在 hook 收到第一次测量结果之前,默认情况下宽度和高度会返回 undefined。
你可以覆盖此行为,这对 SSR 也可能很有用。
const { ref, width = 100, height = 50 } = useResizeObserver<HTMLDivElement>();
在此处,“width”和“height”将分别为 100 和 50,直到 ResizeObserver 开始运行并报告实际尺寸。
不使用默认值
如果您只需要实际测量值(仅来自 ResizeObserver 的值,不包含任何默认值),则可以直接不提供默认值:
const { ref, width, height } = useResizeObserver<HTMLDivElement>();
在此处,“width”和“height”在 ResizeObserver 完成首次测量前将为 undefined。测量维度为 0 时会报告为 0(而非 undefined),因此您可以区分真正零尺寸的元素和尚未测量的元素。
使用 CSS-in-JS 实现容器/元素查询
可以通过 CSS-in-JS 解决方案根据元素的宽度/高度有条件地应用样式,这正是容器/元素查询的基本理念:
polyfill 处理
该库面向现代(ES2020)浏览器,并且不包含 ResizeObserver polyfill。它没有任何运行时依赖。
polyfill 最好在宿主应用中进行处理,而不是在导入的库中,这样消费者可以控制所使用的具体 polyfill。如果您需要支持没有原生 ResizeObserver 的环境,请安装诸如 @juggle/resize-observer 之类的 polyfill,并在钩子运行前使其可用——例如在应用的入口点:
import { ResizeObserver } from "@juggle/resize-observer";
if (!window.ResizeObserver) {
window.ResizeObserver = ResizeObserver;
}
不支持 ResizeObserver 的环境
一种选择是使用 polyfill。另一种选择是在缺少 ResizeObserver 时让该 hook 处于闲置状态,并回退到默认尺寸。
这种方式之所以可行,是因为该 hook 延迟 创建其 ResizeObserver——并非在渲染时创建,而是在首次实际有元素需要观察时才创建。(这也使其支持 SSR。)如果没有要观察的元素,它就不会访问全局对象,因此不会抛出任何错误。Hook 不能有条件地调用,但通过这种方式,你无需这样做。
只需检测一次全局对象,然后包装返回的 ref 回调,以便仅在元素存在时才传递该元素:
const isRoAvailable = typeof window !== "undefined" && "ResizeObserver" in window;
// Stays at 100x50 where there's no ResizeObserver to measure with.
const { ref: observe, width = 100, height = 50 } = useResizeObserver<HTMLDivElement>();
const ref = useCallback(
(element: HTMLDivElement | null) => {
if (isRoAvailable) {
observe(element);
}
},
[observe],
);
如果您已经有一个 ref 对象,同样的方法也适用——只需在全局对象可用时将其传递给该 hook 即可:
const ref = useRef<HTMLDivElement>(null);
const { width = 100, height = 50 } = useResizeObserver<HTMLDivElement>({
ref: isRoAvailable ? ref : null,
});
“ResizeObserver loop limit exceeded”(调整大小观察器循环限制已超出)
如果你遇到此问题——Chrome 中显示 ResizeObserver loop limit exceeded,或 Firefox 中显示 ResizeObserver loop completed with undelivered notifications——尽管它以错误形式呈现,但实际上并无危害。
这意味着观察行为导致了调整大小,而调整大小又引发了另一次观察,浏览器为避免挂起而提前终止了循环。这是一种在布局过程中防止无限循环的保护机制,并非崩溃——未传递的通知会在下一帧中正常送达。
尽管如此,仍建议按以下顺序进行处理:
- 找出根本原因。 通常是调整大小处理逻辑中的某些内容以反馈方式改变了被观察元素的布局。这种反馈循环才是真正的问题,修复它就能彻底消除该提示。
- 在错误跟踪工具中过滤(Sentry、Datadog 等)。 如果你无法确定原因,请停止收集此类错误。它属于干扰信息而非实际故障,任其存在可能会掩盖真正的错误。
- 延迟一帧报告。 最后的解决办法——创建一个通过
requestAnimationFrame推送尺寸信息的钩子:
const useResizeObserverWithRAF = (opts) => {
const [size, setSize] = useState({ width: undefined, height: undefined });
const { ref } = useResizeObserver({
...opts,
onResize: ({ width, height }) => {
requestAnimationFrame(() => setSize({ width, height }));
},
});
return { ref, ...size };
};
这之所以可行,是因为它将尺寸报告移出了强制执行限制的布局阶段。但问题在于,尺寸信息现在会延迟一帧才到达(在 60fps 下约为 16 毫秒),这在很大程度上失去了使用 ResizeObserver 的意义——因此,只有在前两种方案都已尝试且不可行时,才考虑使用这种方法。
相关链接
许可证
MIT