React中将ref存入父组件state是否合理?跨组件获取DOM方案
结论先行
把DOM引用存储在父组件state中本身不是不良实践,属于场景化的可行方案,但你当前的业务场景有更稳妥、更符合React设计逻辑的实现方式,不需要自己手动维护DOM到state的赋值逻辑。
你遇到的核心问题本质是:普通useRef创建的ref对象值变更不会触发组件重渲染,因此动态挂载的ComponentA完成核心DOM挂载后,父组件无法自动感知变化、触发ComponentB的重渲染,这是普通ref的特性限制,和ref转发方案本身无关。
你提出的state存DOM方案的注意事项
这个方案可以正常运行,但有3个容易踩的坑:
- 必须在ComponentA卸载时把对应state的值重置为
null,否则ComponentB可能拿到已经被销毁的DOM引用,读取属性时直接报错,还会造成内存泄漏 - 所有DOM属性的读写操作不要放在组件渲染阶段执行,要放到
useLayoutEffect/useEffect里,避免触发反复的重排重绘 - 不要对存DOM的state做深度比较,DOM是引用类型,浅比较就能判断变化,深度比较只会带来无意义的性能开销
更推荐的实现方案
方案1:回调ref(最轻量,完全匹配你的场景)
React原生支持的回调ref天然就能感知DOM的挂载和卸载时机,不需要你手动在子组件里调用父组件的setState传DOM,实现成本最低:
- 父组件WRAPPER中用state存储目标核心DOM,用
useCallback缓存一个回调ref函数 - 通过ref转发把这个回调ref传给ComponentA,绑定到内部的核心DOM节点上
- DOM挂载时React会自动执行回调、传入DOM节点,DOM卸载时自动传入
null,父组件的state会同步更新,自动触发ComponentB重渲染
示例代码:
function Wrapper() { const [coreDom, setCoreDom] = useState(null); // 缓存回调ref,避免子组件不必要的重渲染 const bindCoreDom = useCallback((node) => { setCoreDom(node); }, []); return ( <div> {shouldRenderA && <ComponentA ref={bindCoreDom} />} <ComponentB targetNode={coreDom} /> </div> ); } // ComponentA通过forwardRef把ref绑定到内部核心DOM const ComponentA = forwardRef((props, ref) => { return ( <div> {/* 其他业务元素 */} <div ref={ref} className="core-element">核心节点</div> </div> ); });
这个方案的可靠性比你手动在子组件生命周期里赋值state更高,React会保证回调ref的执行时机和DOM的挂载/卸载完全同步,不会出现时机错乱。
方案2:所需状态提升(耦合度更低)
如果ComponentB渲染只需要核心DOM的部分属性(比如尺寸、位置、滚动偏移量等),完全不需要把整个DOM引用传递给ComponentB:
- 在WRAPPER层拿到核心DOM后,监听你需要的属性变化(可以配合
ResizeObserver、滚动监听等) - 把需要的纯数据存在state中传递给ComponentB
这种方式下ComponentB只依赖纯数据渲染,不需要感知DOM的存在,组件耦合度更低,也能避免子组件随意操作DOM带来的不可控副作用。
不建议把DOM存在state的场景
如果你的场景符合以下情况,用普通useRef+ref转发即可,把DOM存在state里只会带来无意义的重渲染:
- 拿到DOM引用只是为了调用DOM方法(比如触发聚焦、控制滚动、播放媒体等),不需要触发组件重渲染
- 组件不存在动态挂载/卸载的逻辑,DOM和父组件生命周期完全同步
内容的提问来源于stack exchange,提问作者tomdelahaba
相关产品推荐
相关产品推荐

