先记住这个答案
通常不应在渲染中读取或修改 ref.current 来决定可见输出,因为 ref 变化不触发 React 更新,且渲染可能被重复、暂停或放弃。渲染中写入可变容器会让一次未提交的尝试留下外部可见变化,读取则可能使相同 props 与 state 得到不同输出。官方允许一类可预测的初始化:只在尚未初始化时写入确定的初始对象,后续渲染不重新改变它。这个例外不允许在渲染中任意同步最新 props、递增计数或创建外部订阅。
- 界面需要的数据使用 props 和 state
- 一般命令式 ref 读写放在事件或合适生命周期
- 初始化例外要求可预测且没有外部副作用
为什么 render 中递增 ref 不是可靠渲染计数
如果每次函数执行都修改 ref.current,再把它显示到页面,开发检查或未提交的渲染也会改变这个计数。数值描述的既不是稳定业务状态,也不一定是用户真正看到的提交次数,后续行为会依赖框架调用函数的过程。
下面只在空容器时完成可预测初始化,后续计数变化发生在点击事件里,并通过 state 更新可见文字。Map 用于保存命令式数据,state 承担渲染输入,避免期待 current 变化自动通知页面。
import { useRef, useState } from 'react';
export default function CachedClicks() {
const cache = useRef<Map<string, number> | null>(null);
const [visibleCount, setVisibleCount] = useState(0);
if (cache.current === null) cache.current = new Map();
function remember() {
const store = cache.current!;
const next = (store.get('clicks') ?? 0) + 1;
store.set('clicks', next);
setVisibleCount(next);
}
return <button onClick={remember}>已记录:{visibleCount}</button>;
}连续点击得到一、二,普通重渲染不会重新创建正在使用的 Map。示例用 state 展示计数;对于只需要一个计数的实际业务,直接使用 state 会更简单,这里用两者对照职责。
可预测初始化不等于构造函数可以做任何事
new Map 只是创建当前组件内部的普通数据容器,没有对外订阅或请求。若所谓初始化函数会打开连接、登记全局监听或修改共享对象,即使写在 current 为 null 的判断里,也可能被重复渲染或丢弃的实例影响。
直接写 useRef(createExpensiveObject()) 时,参数表达式仍会在函数每次执行时求值,虽然 React 只使用初次 ref 值。需要避免重复构造且初始化本身纯粹时,才考虑空值守卫;不要把它当成通用资源生命周期替代品。
最新值同步与可见状态应采用不同设计
每次 render 都把 props 写进 ref,以供外部回调读取,可能把尚未提交的值暴露给当前生效的外部过程。应先明确回调需要的是已提交值还是某次事件输入,再选择 Effect、Effect Event 或其他符合所用版本约束的方案。
如果 current 中的数据本身决定页面显示,把它藏在 ref 会让 React 无法知道何时重新计算。外部 store 应提供订阅与快照接口;组件内部显示数据通常进入 state。不要依靠其他偶然更新顺便把可变引用中的新内容刷新出来。
容易答错的地方
- useRef 不触发渲染,所以 render 里修改它一定安全
- 不触发更新只是容器特性,并不提供纯度保证;写入仍可能影响未来读取和其他回调,使未提交渲染留下可观察变化。
- 只要加一个 null 判断就能在 render 创建连接
- null 守卫不能把外部副作用变成纯初始化。连接、观察器和订阅仍需要明确的建立与清理阶段,不能依赖只运行一次的假设。
面试官还会怎么问?
为什么示例中读取 cache.current 的空值检查允许?
它属于可预测初始化的窄例外,只在尚未建立内部容器时给出确定初值,之后不随每次渲染改写。不能把这个例外扩展成任意读取可变业务状态。
读取 ref 的事件处理器能看到后续写入吗?
读取时会访问同一个可变容器的当前内容,这与闭包捕获某次 state 值不同。但它仍不会自动驱动界面,展示变化需要明确的状态更新。
怎样保存需要在页面显示的前一个值?
应在状态转换中明确保存 previous 与 current,或在合适上层维护历史。不要为了省一个状态字段,在 render 中偷读随后又会被 Effect 修改的 ref。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。