React useEffect依赖clientHeight未触发问题求助
问题描述
我有一个包含固定定位header和body的组件,body的marginTop需要根据header的clientHeight值动态设置。通过useRef创建headerRef获取header DOM元素,使用useState管理body的marginTop。更新margin的setState函数置于useEffect中,依赖项为headerRef.current?.clientHeight。当header高度变化导致页面重渲染时,预期useEffect触发并重置body的marginTop,但依赖项未生效,hook未触发,请问该行为的原因是什么?
简化版代码
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>showcase</title> </head> <body> <div id="root"></div> <script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script> <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script> <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> <script src="index.js" type="text/babel" ></script> </body> </html>
index.jsx
const e = React.createElement; function StickyTop() { const headerRef = React.useRef(null); const [margin, setMargin] = React.useState("15px"); const [headerData, setHeaderData] = React.useState( <div style={{ backgroundColor: "#e5e5e5" }}>header data loading...</div> ); React.useEffect(() => { setTimeout( () => setHeaderData( <div style={{ height: "100px", backgroundColor: "#e5e5e5" }}> new header data </div> ), 1000 ); }, []); React.useEffect(() => { if (headerRef.current.clientHeight) { setMargin(headerRef.current.clientHeight); } }, [headerRef.current?.clientHeight]); return ( <main> <div ref={headerRef} style={{ position: "fixed", top: 0 }}> {headerData} </div> <div style={{ marginTop: margin }}>the awsoem body</div> </main> ); } const domContainer = document.querySelector("#root"); const root = ReactDOM.createRoot(domContainer); root.render(e(StickyTop));
原因分析
- useRef的current属性不触发重渲染:React不会追踪
ref.current的变化,headerRef.current始终指向同一个DOM元素,哪怕这个元素的clientHeight属性改变了,ref的引用本身没变,所以React不会认为依赖项有变化。 - useEffect依赖项是浅比较:React对比
useEffect依赖项时,只会做浅层次的引用对比,不会检查DOM元素的属性变化。headerRef.current?.clientHeight是DOM元素的属性,它的改变不会让依赖项的引用发生变化,因此useEffect不会重新执行。
解决方案
方法1:用ResizeObserver监听尺寸变化
直接监听header元素的尺寸变化,一旦高度改变就更新margin:
React.useEffect(() => { const observer = new ResizeObserver(entries => { entries.forEach(entry => { setMargin(entry.contentRect.height); }); }); if (headerRef.current) { observer.observe(headerRef.current); } // 组件卸载时停止监听 return () => { if (headerRef.current) { observer.unobserve(headerRef.current); } }; }, []);
方法2:以headerData为依赖触发更新
既然header高度变化是因为headerData状态更新导致的,直接把headerData作为useEffect的依赖,每次状态更新后获取最新高度:
React.useEffect(() => { // 用setTimeout确保DOM已经更新完成,能拿到最新的clientHeight setTimeout(() => { if (headerRef.current?.clientHeight) { setMargin(headerRef.current.clientHeight); } }, 0); }, [headerData]);
方法3:用useLayoutEffect同步执行
如果需要在DOM更新后立即同步计算高度,用useLayoutEffect替代useEffect,它会在浏览器绘制前执行,避免页面闪烁:
React.useLayoutEffect(() => { if (headerRef.current?.clientHeight) { setMargin(headerRef.current.clientHeight); } }, [headerData]);
内容的提问来源于stack exchange,提问作者Raz Akbari
相关产品推荐
相关产品推荐

