
本文详解如何在 react 中设计一个可靠的 `usewindowsize` 自定义 hook,准确获取整个文档(而非仅视口)的高度和宽度,并解决初始化时 `document.body.offsetheight` 返回 0 或无效值的核心问题。
在 React 中,window.document.body.offsetHeight 用于获取整个 HTML 文档内容区域的实际渲染高度(含滚动内容),而 window.innerHeight 仅代表当前可视区域(viewport)高度。许多开发者在封装 useWindowSize Hook 时发现:若初始 state 直接使用 document.body.offsetHeight,往往得到 0 或远小于预期的值——这是因为组件首次挂载时,DOM 可能尚未完成布局计算(尤其是当
内容依赖异步数据、CSS 加载或动态渲染时),导致 offsetHeight 尚未就绪。根本原因在于:useState 的初始值在组件函数执行阶段即被求值,此时 React 还未将 DOM 提交到页面,document.body 虽存在,但其子元素可能尚未完成样式计算与布局(layout)。而 resize 事件触发时,浏览器已处于稳定渲染状态,因此后续调用 offsetHeight 总是有效的。
✅ 正确解法:延迟初始化 + 主动触发更新
我们不应在 useState 中直接读取 offsetHeight,而应借助 useEffect(它在 DOM 渲染后执行)手动触发一次尺寸采集:
import { useState, useEffect } from 'react';
function useDocumentSize() {
const [size, setSize] = useState<[number, number]>([0, 0]);
useEffect(() => {
let timeoutId: NodeJS.Timeout | null = null;
const updateSize = () => {
clearTimeout(timeoutId!);
timeoutId = setTimeout(() => {
// ✅ 安全读取:此时 DOM 已布局完成
const height = document.body.offsetHeight;
const width = document.body.offsetWidth;
setSize([height, width]);
}, 150); // 防抖,避免频繁重绘
};
// ? 关键:挂载后立即执行一次,确保初始值准确
updateSize();
// 监听窗口大小变化
window.addEventListener('resize', updateSize);
return () => {
window.removeEventListener('resize', updateSize);
if (timeoutId) clearTimeout(timeoutId);
};
}, []); // ? 空依赖数组:仅在挂载/卸载时运行
return size;
}? 使用示例:
function App() {
const [docHeight, docWidth] = useDocumentSize();
return (
);
}⚠️ 注意事项:
- 不要用 innerHeight/innerWidth 替代:它们反映视口尺寸,无法代表长页面的完整文档高度;
- 确保 有明确内容与样式:若 body 为空或 height: 0,offsetHeight 必然为 0;可添加 min-height: 100vh 等兜底样式;
- 服务端渲染(SSR)兼容性:该 Hook 仅适用于浏览器环境,在 SSR 中需加 typeof window !== 'undefined' 判断,或返回默认值 [0, 0];
- 性能优化:防抖 150ms 是合理折中;如需更高实时性,可改用 ResizeObserver(但需注意兼容性)。
? 进阶建议:对于更健壮的尺寸监听,推荐结合 ResizeObserver 观察 document.body 元素本身,避免依赖全局 resize 事件,从而提升精度与响应速度。但对大多数场景,上述 useEffect + resize + 手动触发 方案简洁、可靠、兼容性好,是推荐的最佳实践。










