useRef 与可变引用
useRef 与可变引用 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
这一课给「计时器 id、上一次的值、DOM 节点」这类不参与渲染的数据找一个正经住处:useRef。读完你应该能判断一个值该放 state 还是 ref,并且不会再把界面需要的数字藏进 ref 里导致界面不动。
问题场景
写一个计数器,同事为了「少触发渲染」把计数放进了 ref:
function BadCounter() {
const countRef = useRef(0);
return (
<button onClick={() => { countRef.current += 1; }}>
点了 {countRef.current} 次
</button>
);
}
点十次,界面纹丝不动;随便再点一下别的按钮触发一次渲染,数字又「啪」地跳到 10。值明明在变,界面却像坏了一样。
核心解释
useRef(初始值) 返回一个跨渲染稳定存在的对象 { current: 初始值 }。组件卸载前,每次渲染拿到的都是同一个对象——这是它和普通变量的区别;但修改 ref.current 不会触发重新渲染——这是它和 state 的区别。
于是分工很清楚:
- state 是给界面看的:值的变化要反映到界面上,用 state,React 负责触发渲染。
- ref 是给逻辑用的:界面不需要知道它,只有事件处理和 Effect 需要读写,用 ref。
适合放 ref 的典型数据:setInterval 返回的计时器 id(暂停时拿来清除)、上一次渲染的某个值、DOM 节点引用、「是否已经发起过请求」的标记。它们都是控制逻辑的零件,不是界面内容。
反过来说,凡是 JSX 里要显示、要参与条件渲染的值,放 ref 就是在隐藏必要的更新——React 根本不知道它变了,自然不会重渲染。
示例拆解
正确的秒表:秒数用 state(界面要显示),计时器 id 用 ref(暂停时要清除):
function Stopwatch() {
const [elapsed, setElapsed] = useState(0);
const [running, setRunning] = useState(false);
const timerRef = useRef(null);
useEffect(() => {
if (!running) return;
timerRef.current = setInterval(() => {
setElapsed((s) => s + 1); // 界面数据:state + 函数式更新
}, 1000);
return () => clearInterval(timerRef.current); // 计时器 id:ref
}, [running]);
return (
<div>
<p>已计时:{elapsed} 秒</p>
<button onClick={() => setRunning((r) => !r)}>
{running ? '暂停' : '开始'}
</button>
</div>
);
}
elapsed出现在 JSX 里,必须是 state,改动才会重新渲染。timerRef.current只在清理时被clearInterval用到,界面从不显示它,ref 正合适。- 顺手遵守了 R30 会展开的规矩:
setElapsed(s => s + 1)用函数式更新,回调里不依赖旧快照。
动手任务
打开本课实验区:
- 先连点十次「ref +1」按钮,观察界面不变;再点一次「state +1」触发渲染,看 ref 计数突然跳出来——体会「藏更新」的后果。
- 给秒表加一个「重置」按钮:停表并把
elapsed归零,注意重置时也要清掉进行中的 interval。 - 把
timerRef换成 useState 存计时器 id,功能不变——然后想一下:这里用 ref 好在哪?(提示:id 变化不需要渲染。)
常见错误
- 界面数据放 ref:症状是「点了没反应,下次随便什么操作后数字乱跳」。原因就是改
ref.current不触发渲染。 - 在渲染期间读写 ref:组件函数体里直接
ref.current = xxx或读它做计算,会让多次渲染结果不一致;ref 的读写应发生在事件处理和 Effect 里。 - 指望改 ref 触发 Effect:
useEffect(() => {...}, [myRef])的依赖是 ref 对象本身,每次渲染都一样;想监听变化请用 state。 - 把 ref 当「绕过渲染的性能工具」:渲染慢应该用 useMemo、拆组件解决,用 ref 藏状态只是把 bug 推迟。
检查与复盘
完成下方检查题确认理解。自查清单:
- 一句话说清分工:界面要跟着变的用 state,纯逻辑用的用 ref
- 知道 ref 对象跨渲染稳定、改 current 不触发渲染这两条性质
- 写代码前会问一句:这个值 JSX 里用不用?用,就是 state
参考资料
- React 官方文档:用 useRef 引用值 —— ref 的适用场景与「不要在渲染期间读写」的官方说明
- React 官方文档:useRef —— API 细节与常见用法
- MDN:setInterval —— 计时器 id 的来源与清除方式
读完这一课了?
标记完成后,工作台会安排接下来的学习。