一句话定义
const ref = useRef(initial) 返回一个跨渲染稳定、修改不触发重渲染的可变盒子:挂到 DOM 元素上就是句柄(ref.current 指向节点),用作普通容器则是「渲染之外的备忘录」。
为什么重要
React 是声明式的,但现实世界有命令式需求:聚焦、滚动、测量、播放器、第三方插件、requestAnimationFrame 循环。ref 是官方承认的逃生舱;同时它也是理解「快照模型 vs 可变现实」的对照物——用错 ref 会写出绕过渲染系统的幽灵状态。
前置知识
核心概念
- 三种用途:① DOM 句柄;② 跨渲染存可变值(定时器 id、上一次值);③ 自定义 Hook 内部状态(不暴露给渲染)。
- 写 ref 不触发渲染:
ref.current = x只是改盒子;界面不会因此更新。 - 读 ref 在渲染期间不可靠:渲染应只读 props/state;渲染中读写 ref 违反纯函数纪律。
- forwardRef / ref 作为 prop:React 19 起
ref可以直接当 prop 传(无需 forwardRef);19 之前子组件必须用 forwardRef 转发。 - useImperativeHandle:父组件只暴露受控的命令式 API 子集,而非整个 DOM 节点。
原理 / 机制
DOM 句柄的经典用法:
jsxfunction SearchBox() {
const inputRef = useRef(null);
function open() {
inputRef.current.focus(); // 提交后 ref 已指向真实节点
inputRef.current.select();
}
return (
<>
<input ref={inputRef} defaultValue="" />
<button onClick={open}>搜索</button>
</>
);
}
时序要点:渲染期间 ref.current 是 null(DOM 还没提交);首次读到值最早在 useLayoutEffect/事件回调里。
作为「最新值盒子」解决 kp-014 的 stale closure(与 useCallback 的取舍):
jsxfunction useInterval(callback, delay) {
const saved = useRef(callback);
useEffect(() => { saved.current = callback; }); // 每次渲染刷新引用
useEffect(() => {
if (delay === null) return;
const id = setInterval(() => saved.current(), delay);
return () => clearInterval(id);
}, [delay]); // delay 变才重建 interval;callback 永远最新
}
ref vs state 的判断题:
| 问题 | 答案指向 |
|---|---|
| 这个值变化后需要重渲染吗? | 需要 → state;不需要 → ref |
| 这个值要在渲染输出里出现吗? | 出现 → state;只在事件/异步里用 → ref |
| 是 DOM 命令式操作吗? | 是 → ref |
「上一次值」模式(比较渲染间差异):
jsxfunction usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value; // 在提交后更新 → 渲染时读到的是"上一次"的值
}, [value]);
return ref.current;
}
直观类比
state 是演出的剧本:改一页就要重新走台(重渲染),观众看到的是新戏。ref 是后台工作群的便签:导演随手记「灯 3 号再调亮些」,不触发重新走台,但执行时(effect/事件)人人能看最新便签。把便签内容写到剧本上(用 ref 值渲染界面)= 演出和后台对不上号——这就是 ref 幽灵 bug。
实例 / 案例
useImperativeHandle:封装视频播放器,只暴露安全 API:
jsxfunction Player({ ref }) {
const videoRef = useRef(null);
useImperativeHandle(ref, () => ({
play: () => videoRef.current.play(),
pause: () => videoRef.current.pause(),
seek: s => { videoRef.current.currentTime = s; },
}), []); // 暴露的函数集固定
return <video ref={videoRef} src="/demo.mp4" />;
}
// 父组件:拿到的是 {play, pause, seek},碰不到原始 <video> 节点
(React 19 直接用 function Player({ ref });19 前写法为 forwardRef((props, ref) => ...)。)
滚动联动列表:
jsxfunction Chat({ messages }) {
const endRef = useRef(null);
useEffect(() => {
endRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages.length]); // 新消息到达 → 滚到底部
return (
<ul>
{messages.map(m => <li key={m.id}>{m.text}</li>)}
<li ref={endRef} aria-hidden="true" />
</ul>
);
}
常见误区
- 用 ref 存「界面需要显示的值」:改了不更新、更新了不同步,各种诡异时序 bug;界面值一律 state。
- 渲染期间写 ref:
if (!ref.current) ref.current = compute()(缓存计算结果)——并发渲染下同一渲染可能执行多次/被丢弃,缓存会被污染;缓存计算用 useMemo(kp-018)。 - 忘判 null:条件渲染的元素 ref 可能没挂载;
ref.current?.focus()。 - 暴露整个 DOM 节点:父组件能对子组件 DOM 任意操作,破坏封装;用 useImperativeHandle 白名单。
- 以为 ref.current 是响应式的:它不会让 JSX 更新,也不会出现在依赖数组的追踪里——想「值变了触发 effect」就得配 state。
自测题
- 说出 ref 与 state 在「触发渲染」上的本质区别,并各举一个正确用例。
- 为什么「渲染期间缓存计算结果到 ref」在并发渲染下不安全?正确工具是什么?
- useInterval 案例里两个 effect 各自解决什么问题?
- 实现
usePrevious:为什么更新放在 effect 里而不是渲染期间? - React 19 中 forwardRef 的地位发生了什么变化?
与其他知识点的关系
- 向前:是 kp-011 非受控、kp-014 「逃出快照」、kp-015
useInterval等模式的底层工具。 - 向后:kp-020 中 Suspense 出现前的测量类需求全靠它;kp-019 的性能测量(getBoundingClientRect)也走 ref;kp-024 中 ref 在 Server Component 里不可用(无浏览器)——这是 RSC 边界的判据之一。
延伸阅读
- Referencing Values with Refs:<https://react.dev/learn/referencing-values-with-refs>
- Manipulating the DOM with Refs:<https://react.dev/learn/manipulating-the-dom-with-refs>
- useImperativeHandle:<https://react.dev/reference/react/useImperativeHandle>