
一句话:
ref是「绕过 React 数据流直接访问 DOM 或保存可变值」的手段;访问 DOM 要用useRef+ref={ref}(函数组件不能直接接收ref,需要forwardRef,React 19 起可以直接接收)。
一、ref 的三种形态
// ① 对象 ref(最常用):useRef 返回的 { current: ... } 对象
const inputRef = useRef(null);
;
// ② 回调 ref:传入一个函数,挂载时调用(参数为 DOM),卸载时以 null 调用
{ if (el) el.focus(); }} />;
// ③ 字符串 ref(❌ 已废弃,不要用)
;
this.refs.myInput;
// 回调 ref 的两种写法(React 19 起支持返回清理函数)
// 1) 简单赋值
{ inputRef.current = el; }} />
// 2) 带清理(React 19+,适合需要「解绑」的场景)
{
// 挂载
el?.focus();
return () => {
// 卸载(或 ref 变化时)
console.log('cleanup');
};
}}
/>
| 形态 | 写法 | 特点 |
|---|---|---|
| 对象 ref | useRef(null) + ref={obj} |
最常用,读写 ref.current |
| 回调 ref | ref={(el) => ...} |
灵活,可在挂载/卸载时执行逻辑 |
| 字符串 ref | ref="x" |
❌ 已废弃(性能与可读性问题) |
二、useRef 返回的对象
const ref = useRef(null);
console.log(ref); // { current: null }(首次渲染)
// 挂载后:{ current: }
useRef 的三个特性:
| 特性 | 说明 |
|---|---|
| 值在渲染之间保持不变 | 引用同一个对象(ref 本身不变) |
修改 current 不触发重渲染 |
它不参与渲染流程(这是与 useState 的根本差异) |
| 每次渲染拿到同一个对象 | 所以可以用来存「跨渲染的变量」 |
// useRef vs useState 的对比
function Compare() {
const [count, setCount] = useState(0); // 变化 → 触发重渲染
const refCount = useRef(0); // 变化 → 不触发重渲染
const handleClick = () => {
setCount(count + 1); // UI 会更新
refCount.current++; // UI 不更新,但值已变
console.log(refCount.current); // 累加正常
};
return (
<>
state: {count}
ref: {refCount.current}
{/* ⚠️ 不主动更新 */}
>
);
}
| 维度 | useState |
useRef |
|---|---|---|
| 触发重渲染 | ✅ | ❌ |
| 渲染中使用 | ✅(值随渲染更新) | ⚠️ 不应用它决定渲染结果 |
| 适合存 | 影响 UI 的数据 | 与 UI 无关的可变值 |
| 典型用途 | 计数器、表单值、列表 | 定时器 ID、DOM 引用、上一次的值 |
三、访问 DOM 节点
function AutoFocusInput() {
const inputRef = useRef(null);
useEffect(() => {
// ⭐ 必须在 useEffect / 事件回调里访问(此时 DOM 已挂载)
inputRef.current?.focus();
}, []);
return ;
}
访问时机(重要):
① 首次渲染(render 阶段):ref.current 还是 null
→ 组件函数执行时 DOM 还没创建
② 挂载后的 commit 阶段:ref.current 被赋值为 DOM
→ useEffect / useLayoutEffect 里可以访问
③ 卸载时:ref.current 被置回 null
→ 清理函数里不要再访问它
// ❌ 在渲染中访问 ref
function Bad() {
const ref = useRef(null);
console.log(ref.current); // 首次渲染是 null
return ;
}
// ✅ 在 Effect 或事件处理里访问
function Good() {
const ref = useRef(null);
useEffect(() => {
console.log(ref.current); // ✅ DOM 已存在
}, []);
const handleClick = () => {
console.log(ref.current?.getBoundingClientRect()); // ✅
};
return ;
}
// 需要「在 DOM 更新后立刻读取布局」的场景用 useLayoutEffect
function MeasureComponent() {
const ref = useRef(null);
const [height, setHeight] = useState(0);
useLayoutEffect(() => {
// ⭐ 在浏览器绘制前执行 → 不会出现「先渲染错误高度再修正」的闪烁
if (ref.current) setHeight(ref.current.offsetHeight);
}, []);
return 高度:{height};
}
四、在函数组件上使用 ref(forwardRef)
// ❌ 函数组件不能直接接收 ref
function MyInput({ ref }) { // ⚠️ React 18 及之前:ref 不会出现在 props 里
return ;
}
// ✅ React 18 及之前:用 forwardRef 转发
const MyInput = forwardRef(function MyInput(props, ref) {
return ;
});
// 使用
const inputRef = useRef(null);
;
inputRef.current?.focus();
// ✅ React 19:函数组件可以直接把 ref 当普通 prop 接收
function MyInput({ ref, ...props }: Props & { ref?: React.Ref }) {
return ;
}
// forwardRef 仍然可用(向后兼容),但不再必需
| 版本 | 写法 |
|---|---|
| React 18 及之前 | 必须用 forwardRef((props, ref) => ...) |
| React 19+ | 可以把 ref 当普通 prop 接收;forwardRef 仍兼容 |
一句话记忆:
ref不是普通 prop,它是一个特殊的保留字段——历史上 React 把它从 props 里「摘出去」单独处理,所以函数组件拿不到,必须forwardRef;React 19 起这个限制取消了。
五、useImperativeHandle:暴露自定义的命令式接口
// 场景:父组件需要调用子组件的方法(如「重置」「聚焦」「校验」)
const FancyInput = forwardRef(function FancyInput(props, ref) {
const inputRef = useRef(null);
const [value, setValue] = useState('');
useImperativeHandle(ref, () => ({
// ⭐ 只暴露想暴露的方法,而不是把整个 DOM 交出去
focus() {
inputRef.current?.focus();
},
clear() {
setValue('');
},
getValue() {
return value;
},
}), [value]);
return setValue(e.target.value)} />;
});
// 父组件
function Parent() {
const handleRef = useRef(null);
return (
<>
>
);
}
useImperativeHandle 的取舍:
✅ 适合:需要「命令式」的操作(focus、scrollTo、play/pause、reset、validate)
❌ 不适合:用它来「传递数据」——数据应该用 props / 回调(保持单向数据流)
⚠️ 过度使用会让组件「黑盒化」(父组件依赖子组件的内部方法),
难以测试与重构。优先考虑「用 props 驱动状态」而不是「用 ref 调用方法」。
六、ref 的其它常见用途
// ① 保存定时器 / 订阅 ID(不需要触发渲染)
const timerRef = useRef();
useEffect(() => {
timerRef.current = window.setInterval(tick, 1000);
return () => clearInterval(timerRef.current);
}, []);
// ② 保存「上一次的值」
function usePrevious(value: T): T | undefined {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// 使用
const prevCount = usePrevious(count);
现在是 {count},之前是 {prevCount}
// ③ 保存「不需要触发渲染」的可变值(如计数器、标志位)
const renderCount = useRef(0);
renderCount.current++;
// ④ 保存 DOM 尺寸 / 滚动位置
const scrollTopRef = useRef(0);
const handleScroll = (e) => { scrollTopRef.current = e.currentTarget.scrollTop; };
// ⑤ 保存「是否首次渲染」的标记
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return; // 跳过首次
}
doSomething();
}, [dep]);
七、多个 ref 与 ref 数组
// ✓ 场景:需要收集一组元素的 ref(如表单字段校验、虚拟列表测量)
function ItemList({ items }: { items: Item[] }) {
const itemRefs = useRef
// ⚠️ 不要用「数组 + 索引」存 ref(列表变动时索引会错位)
const refs = useRef([]);
{items.map((it, i) => { refs.current[i] = el!; }} />)}
// ✅ 用 Map + 稳定 ID(如上)
八、常见坑
// ① 在渲染中读取 ref.current
const el = ref.current; // ⚠️ 首次渲染是 null
return {el?.offsetHeight};
// ② 忘记判空
ref.current.focus(); // ❌ 可能为 null
// ③ 用 ref 保存「应该触发渲染」的数据
const valueRef = useRef('');
valueRef.current = input; // ⚠️ UI 不会更新,应该用 useState
// ④ 函数组件直接接收 ref(React 18 及之前)
function Comp({ ref }) {} // ⚠️ 拿不到 ref,需要 forwardRef
// ⑤ 在 useEffect 的清理函数里访问 ref.current
return () => { ref.current?.focus(); }; // ⚠️ 此时已为 null(DOM 已卸载)
// ⑥ 用字符串 ref
// ❌ 已废弃
// ⑦ 用 ref 做「数据传递」(应该用 props)
// ⚠️ 反模式
// ⑧ useImperativeHandle 没写依赖数组,导致返回新对象
useImperativeHandle(ref, () => ({ ... })); // ⚠️ 每次渲染都重建(建议加依赖)
// ✅
useImperativeHandle(ref, () => ({ focus() {...} }), []);
// ⑨ 把 ref 直接传给「非 DOM 元素」的自定义组件(未 forwardRef)
// ⚠️ 警告:Function components cannot be given refs
// ⑩ 在 SSR 中访问 DOM
if (typeof window !== 'undefined') { ... } // ✅ 判断环境
面试延伸
- 「
useRef和useState有什么区别?」
最根本的区别是「改变时是否触发重渲染」:useState 的值变化会触发组件重新渲染,且每次渲染拿到的是新的快照值;useRef 返回的对象在渲染之间保持不变,修改 ref.current 不触发渲染,也不参与渲染结果的计算。因此 useRef 适合存「与 UI 无关的可变值」(定时器 ID、DOM 引用、上一次的值、标志位)。
- 「什么时候能访问到
ref.current?」
① 首次渲染的 render 阶段:ref.current 是 null(DOM 还没创建);② commit 阶段之后(即 useEffect / useLayoutEffect 里):ref.current 已经是 DOM 节点;③ 组件卸载后:被置回 null。所以永远不要在渲染函数里读 ref.current,也不要在清理函数里访问它(那时已经为 null)。
- 「为什么函数组件不能直接接收
ref?」
因为历史上 React 把 ref 当作特殊的保留字段——它不会出现在 props 里,而是被 React 单独「摘出去」处理(用于挂载后赋值)。所以函数组件拿不到它,必须用 forwardRef 显式转发给内部的 DOM 或子组件。React 19 起这个限制取消了,ref 可以作为普通 prop 接收(forwardRef 仍向后兼容)。
- 「
forwardRef是做什么的?」
它让函数组件能把外部传入的 ref 转发到内部的某个 DOM 元素或子组件上。用法是 forwardRef((props, ref) => <input ref={ref} {...props} />)。常见场景是「基础组件」(Button、Input)——使用方需要能拿到真实 DOM 做聚焦、测量或与第三方库集成。React 19 起可以直接把 ref 当 props 接收,不再强制使用它。
- 「
useImperativeHandle用在哪?」
当父组件需要命令式地调用子组件的方法时(如 focus()、clear()、reset()、validate()、scrollTo())。它的价值是「只暴露想暴露的接口」,而不是把整个 DOM 节点交出去:useImperativeHandle(ref, () => ({ focus() {...} }))。但要谨慎——过度使用会让组件变成「黑盒」(父组件依赖内部实现),优先考虑用 props 驱动状态。
- 「为什么不能把 ref 当作数据传递的手段?」
因为 ref 的目的是「绕过 React 数据流访问 DOM 或保存可变值」,而数据流应该走 props / 回调 / Context。用 ref 传数据的后果:① 破坏单向数据流(谁改了数据不可追踪);② 不会触发重渲染(改了也不会更新 UI,除非碰巧有别的更新);③ 难以测试与调试。所以 ref 只用于「DOM 访问」与「与 UI 无关的可变值」。
- 「列表里的多个元素怎么收集 ref?」
三种方式:① useRef<Map<id, Element>> + 回调 ref(推荐,卸载时能精确删除);② useRef<Element[]>([]) + 索引(⚠️ 列表增删排序时索引会错位,不推荐);③ 在回调 ref 里用 el.dataset 或闭包传 ID。推荐用 Map 是因为它用「稳定 ID」作键,列表变动不会错位。
- 「需要读取 DOM 布局信息时,用
useEffect还是useLayoutEffect?」
用 useLayoutEffect。因为它在浏览器绘制前同步执行——如果你在 useEffect(绘制后异步执行)里测量尺寸再 setState,用户会先看到「错误布局」再看到「修正后的布局」,产生闪烁。而 useLayoutEffect 在绘制前完成测量与更新,用户看不到中间状态。代价是它会阻塞绘制,所以只用于「确实需要同步读取/写入布局」的场景。
一句话速记
ref是「绕过数据流访问 DOM / 保存可变值」的手段;useRef返回的对象跨渲染保持同一引用,改.current不触发渲染(这是与useState的根本差异);ref.current在首次渲染时是null,只能在useEffect/ 事件回调里访问;函数组件要用forwardRef转发 ref(React 19 起可以直接当 prop);需要暴露命令式接口用useImperativeHandle(只暴露必要方法);别用 ref 传数据(那是 props 的职责)。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!