一、问题背景与核心结论:理清 refs 与 render 的关系
1.1 问题背景:在 React中,是否可以在render 方法中访问refs?为什么?
在日常 React 开发中,我们经常需要通过 refs 直接操作 DOM 或子组件实例。一个常见的疑问随之而来:在 React中,是否可以在render 方法中访问refs?为什么? 这个问题看似简单,却涉及到 React 渲染机制、生命周期以及函数式编程理念的核心。许多初学者在 render 方法中尝试通过 this.refs.xxx 或 ref.current 来读取节点信息,结果却遭遇 null 或与预期不一致的值,甚至引发难以排查的 bug。本文将系统性地回答这一问题,并给出可落地的正确写法。
1.2 核心结论:不建议且不应当在 render 中访问 refs
结论先行:不应当在 render 方法中访问 refs。React 官方文档也明确建议避免在 render 期间访问 refs。原因可以归纳为三点:
- refs 在组件挂载完成后(commit 阶段)才会被赋值,render 执行时 refs 可能尚未就绪;
- render 应当是纯函数,访问 refs 会引入副作用,破坏可预测性;
- 在 render 中读取 refs 容易拿到过时或错误的数据,造成渲染不一致。
1.3 Refs 的基本概念:Refs 是什么、何时被赋值
Refs 是 React 提供的一种 "逃生舱" 机制,用于访问 DOM 节点或类组件实例。常见的创建方式有三种:
- React.createRef():类组件中常用,返回一个 { current: null } 对象;
- 回调 Refs:ref={el => this.myRef = el},在挂载和卸载时被调用;
- useRef Hook:函数组件中使用,返回一个可变的 ref 对象。
关键点在于:ref 对象的 current 属性是在 commit 阶段才被赋值的,而非 render 阶段。这是理解 "为何不能在 render 中访问 refs" 的基石。
二、为什么不能在 render 中访问 refs:从机制到原理
2.1 Refs 的赋值时机:render 阶段 refs 尚未就绪
React 的工作流程可以简化为两个阶段:Render 阶段(协调)与 Commit 阶段(提交)。在 Render 阶段,React 会计算新的虚拟 DOM 树,但此时真实的 DOM 尚未更新,refs 也尚未被赋值。只有进入 Commit 阶段,React 才会完成 DOM 的更新并把节点实例挂到 ref 上。因此在 render 方法执行时,ref.current 通常还是 null,或者指向 "上一次" 的旧节点。
2.2 render 应当保持纯函数特性:访问 refs 破坏纯度
React 设计哲学强调 render 应当是一个纯函数:相同的 props 和 state 应当产生相同的输出。如果我们在 render 中读取 refs,就相当于让渲染结果依赖于 "组件外部的可变状态" 或 "上一次渲染的 DOM 状态",这会让组件变得难以预测、难以测试,也容易在 React 的并发模式(Concurrent Mode)下引发难以复现的 bug。访问 refs、修改 refs、触发网络请求等都属于副作用,应当放在 componentDidMount、componentDidUpdate 或 useEffect 中。
2.3 时机错乱导致的问题:拿到的可能是过时的数据
即便在某些场景下 refs 已经被赋值(比如组件已经挂载,进行再次 render),在 render 中读取 refs 仍存在风险。因为此时 DOM 还没有更新到本次 render 对应的状态,refs 指向的是 "上一次提交" 的节点。例如你想根据某个输入框的当前宽度来决定布局,但在 render 中读取到的宽度其实是上一次的值,会导致界面 "慢一拍" 甚至抖动。
2.4 流程图解析:render 与 refs 赋值的时间线
下面通过 Mermaid 流程图直观展示 React 渲染流程中 refs 被赋值的时机:
#publish-mermaid-1785932230141-0{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#publish-mermaid-1785932230141-0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#publish-mermaid-1785932230141-0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#publish-mermaid-1785932230141-0 .error-icon{fill:#552222;}#publish-mermaid-1785932230141-0 .error-text{fill:#552222;stroke:#552222;}#publish-mermaid-1785932230141-0 .edge-thickness-normal{stroke-width:1px;}#publish-mermaid-1785932230141-0 .edge-thickness-thick{stroke-width:3.5px;}#publish-mermaid-1785932230141-0 .edge-pattern-solid{stroke-dasharray:0;}#publish-mermaid-1785932230141-0 .edge-thickness-invisible{stroke-width:0;fill:none;}#publish-mermaid-1785932230141-0 .edge-pattern-dashed{stroke-dasharray:3;}#publish-mermaid-1785932230141-0 .edge-pattern-dotted{stroke-dasharray:2;}#publish-mermaid-1785932230141-0 .marker{fill:#333333;stroke:#333333;}#publish-mermaid-1785932230141-0 .marker.cross{stroke:#333333;}#publish-mermaid-1785932230141-0 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#publish-mermaid-1785932230141-0 p{margin:0;}#publish-mermaid-1785932230141-0 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#publish-mermaid-1785932230141-0 .cluster-label text{fill:#333;}#publish-mermaid-1785932230141-0 .cluster-label span{color:#333;}#publish-mermaid-1785932230141-0 .cluster-label span p{background-color:transparent;}#publish-mermaid-1785932230141-0 .label text,#publish-mermaid-1785932230141-0 span{fill:#333;color:#333;}#publish-mermaid-1785932230141-0 .node rect,#publish-mermaid-1785932230141-0 .node circle,#publish-mermaid-1785932230141-0 .node ellipse,#publish-mermaid-1785932230141-0 .node polygon,#publish-mermaid-1785932230141-0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#publish-mermaid-1785932230141-0 .rough-node .label text,#publish-mermaid-1785932230141-0 .node .label text,#publish-mermaid-1785932230141-0 .image-shape .label,#publish-mermaid-1785932230141-0 .icon-shape .label{text-anchor:middle;}#publish-mermaid-1785932230141-0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#publish-mermaid-1785932230141-0 .rough-node .label,#publish-mermaid-1785932230141-0 .node .label,#publish-mermaid-1785932230141-0 .image-shape .label,#publish-mermaid-1785932230141-0 .icon-shape .label{text-align:center;}#publish-mermaid-1785932230141-0 .node.clickable{cursor:pointer;}#publish-mermaid-1785932230141-0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#publish-mermaid-1785932230141-0 .arrowheadPath{fill:#333333;}#publish-mermaid-1785932230141-0 .edgePath .path{stroke:#333333;stroke-width:1px;}#publish-mermaid-1785932230141-0 .flowchart-link{stroke:#333333;fill:none;}#publish-mermaid-1785932230141-0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#publish-mermaid-1785932230141-0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#publish-mermaid-1785932230141-0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#publish-mermaid-1785932230141-0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#publish-mermaid-1785932230141-0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#publish-mermaid-1785932230141-0 .cluster text{fill:#333;}#publish-mermaid-1785932230141-0 .cluster span{color:#333;}#publish-mermaid-1785932230141-0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#publish-mermaid-1785932230141-0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#publish-mermaid-1785932230141-0 rect.text{fill:none;stroke-width:0;}#publish-mermaid-1785932230141-0 .icon-shape,#publish-mermaid-1785932230141-0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#publish-mermaid-1785932230141-0 .icon-shape p,#publish-mermaid-1785932230141-0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#publish-mermaid-1785932230141-0 .icon-shape .label rect,#publish-mermaid-1785932230141-0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#publish-mermaid-1785932230141-0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#publish-mermaid-1785932230141-0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#publish-mermaid-1785932230141-0 .node .neo-node{stroke:#9370DB;}#publish-mermaid-1785932230141-0 [data-look=\”neo\”].node rect,#publish-mermaid-1785932230141-0 [data-look=\”neo\”].cluster rect,#publish-mermaid-1785932230141-0 [data-look=\”neo\”].node polygon{stroke:#9370DB;filter:drop-shadow(1px 2px 2px rgba(185, 185, 185, 1));}#publish-mermaid-1785932230141-0 [data-look=\”neo\”].swimlane.cluster rect{filter:none;}#publish-mermaid-1785932230141-0 [data-look=\”neo\”].node path{stroke:#9370DB;stroke-width:1px;}#publish-mermaid-1785932230141-0 [data-look=\”neo\”].node .outer-path{filter:drop-shadow(1px 2px 2px rgba(185, 185, 185, 1));}#publish-mermaid-1785932230141-0 [data-look=\”neo\”].node .neo-line path{stroke:#9370DB;filter:none;}#publish-mermaid-1785932230141-0 [data-look=\”neo\”].node circle{stroke:#9370DB;filter:drop-shadow(1px 2px 2px rgba(185, 185, 185, 1));}#publish-mermaid-1785932230141-0 [data-look=\”neo\”].node circle .state-start{fill:#000000;}#publish-mermaid-1785932230141-0 [data-look=\”neo\”].icon-shape .icon{fill:#9370DB;filter:drop-shadow(1px 2px 2px rgba(185, 185, 185, 1));}#publish-mermaid-1785932230141-0 [data-look=\”neo\”].icon-shape .icon-neo path{stroke:#9370DB;filter:drop-shadow(1px 2px 2px rgba(185, 185, 185, 1));}#publish-mermaid-1785932230141-0 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}是否风险推荐
State/Props 变化
Render 阶段:执行 render 方法
此时访问 refs?
ref.current 为 null 或指向旧节点数据不可靠
Commit 阶段:更新真实 DOM
将 DOM 节点/实例 赋值给 ref.current
componentDidMount / componentDidUpdate / useEffect 触发
在此处安全访问 refs
渲染不一致、难以排查的 bug
稳定、可预测的渲染
从图中可以清楚看到:render 阶段访问 refs 是 "抢跑",而真正的安全时机是 Commit 阶段之后。
三、正确的使用方式:在合适的时机访问 refs
3.1 在生命周期中访问 refs:类组件的最佳实践
在类组件中,应当在 componentDidMount 与 componentDidUpdate 中访问 refs。这两个生命周期都在 Commit 阶段之后执行,此时 DOM 已经更新完毕,refs 已经指向最新的节点。例如,要在挂载后聚焦一个输入框:
class AutoFocusInput extends React.Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
componentDidMount() {
// 正确:在 commit 之后访问 refs
this.inputRef.current.focus();
}
render() {
// 错误示范:不要在这里访问 this.inputRef.current
return <input ref={this.inputRef} type="text" />;
}
}
如果在 render 中读取 this.inputRef.current,首次渲染时它必然是 null,没有任何意义。
3.2 函数组件中使用 useRef 与 useEffect:Hook 时代的写法
函数组件没有生命周期,但可以通过 useEffect 来模拟 "挂载后访问 refs" 的时机。useEffect 的回调在 DOM 更新之后执行,是访问 refs 的安全位置:
import React, { useRef, useEffect } from 'react';
function AutoFocusInput() {
const inputRef = useRef(null);
useEffect(() => {
// 正确:在 effect 中访问 ref
inputRef.current.focus();
}, []);
return <input ref={inputRef} type="text" />;
}
需要注意:不要在组件主体(相当于 render)中直接读写 inputRef.current,这等同于在 render 中访问 refs,存在同样的风险。
3.3 回调 Refs 与 createRef 的对比:灵活性与时机的取舍
除了 createRef 与 useRef,React 还提供 "回调 Refs" 的形式。回调 Refs 会在节点挂载时以 DOM 作为参数调用,在卸载时以 null 调用。它可以让你在节点就绪的瞬间执行逻辑,但要避免在回调中触发会导致再次渲染的状态更新:
function MeasureInput() {
const measureRef = (node) => {
if (node !== null) {
// node 已就绪,可在此读取尺寸等信息
console.log('width:', node.getBoundingClientRect().width);
}
};
return <input ref={measureRef} type="text" />;
}
回调 Refs 适合 "需要根据节点信息立即做一些不涉及渲染的逻辑" 的场景,但若需要更新状态,仍应通过 useEffect 来触发,避免在 render 链路中造成循环。
3.4 最佳实践总结:把 refs 访问放到 commit 之后
综合以上分析,可以提炼出几条最佳实践:
回到最初的问题——在 React中,是否可以在render 方法中访问refs?为什么? 答案已经清晰:技术上或许能拿到值(在非首次渲染时),但从机制、理念与稳定性三个维度来看都不应当这么做。把 refs 当作 "提交后的产物",让 render 保持纯粹,才能写出可预测、易维护的 React 代码。