useRef、forwardRef 和 useImperativeHandle 有什么区别?
简化版
useRef 用来保存不触发渲染的可变值或 DOM 引用;forwardRef 让父组件能把 ref 传给子组件内部节点;useImperativeHandle 用来限制或定制暴露给父组件的命令式方法。它们都属于“逃离声明式”的工具,要克制使用。
详细版
useRef 常用于访问 DOM:
const inputRef = useRef<HTMLInputElement>(null);
function focus() {
inputRef.current?.focus();
}
forwardRef 让自定义组件接收 ref:
const Input = forwardRef<HTMLInputElement, Props>((props, ref) => {
return <input ref={ref} {...props} />;
});
useImperativeHandle 暴露自定义能力:
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
}));
面试重点是:ref 改变不会触发渲染,不要用它替代 state。
完整版教学
一、useRef 保存的是跨渲染稳定的容器
React 函数组件每次渲染都会重新执行函数体。
普通局部变量会重新创建,而 useRef 返回的对象在多次渲染之间保持稳定。
它的核心形状是 { current: value }。
const countRef = useRef(0);
function click() {
countRef.current += 1;
}
如果点击 5 次,countRef.current 可以从 0 变成 5。
但这个变化不会触发组件重新渲染。
所以 ref 适合保存“需要记住但不需要显示驱动 UI”的值。
二、useRef 常见用途是 DOM 引用和非渲染数据
最常见用途是拿 DOM 节点,比如聚焦输入框、读取尺寸、控制滚动。 另一个用途是保存定时器 id、上一次值、请求标记等不需要渲染的可变数据。 这类数据如果放 state,反而会引发不必要渲染。
const timerRef = useRef<number | null>(null);
useEffect(() => {
timerRef.current = window.setInterval(() => {
console.log('tick');
}, 1000);
return () => {
if (timerRef.current) clearInterval(timerRef.current);
};
}, []);
这里的 1000 表示每秒执行一次。
定时器 id 只用于清理,不参与 UI 展示,所以适合放 ref。
如果 UI 要显示秒数,就应该用 state。
三、ref 改变不触发渲染,这是优势也是陷阱
useRef 的更新不会让 React 重新渲染。
这能避免无意义更新,但如果你把 UI 依赖的数据放进 ref,页面就不会自动刷新。
这也是面试高频陷阱。
const countRef = useRef(0);
return <button>{countRef.current}</button>;
点击时只改 countRef.current,按钮文本不会自动更新。
如果要让文本从 0 变 1、2、3,应该用 useState。
判断标准很简单:数据变化需要反映到 JSX,就用 state;只需要被事件或 Effect 读取,可以用 ref。
四、forwardRef 解决 ref 不能直接穿透自定义组件的问题
父组件给普通 DOM 元素传 ref 可以直接拿到节点。
但给函数组件传 ref 时,React 默认不知道你想拿哪个内部节点。
forwardRef 让子组件显式接收并转发这个 ref。
const SearchInput = forwardRef<HTMLInputElement>((props, ref) => {
return <input ref={ref} placeholder="搜索" />;
});
function Page() {
const inputRef = useRef<HTMLInputElement>(null);
return <SearchInput ref={inputRef} />;
}
| 工具 | 解决问题 | 常见场景 |
|---|---|---|
useRef | 保存 ref 容器 | DOM、定时器、上次值 |
forwardRef | 让子组件接收父 ref | 组件库输入框、弹窗 |
useImperativeHandle | 定制暴露内容 | 只暴露 focus、reset |
组件库里这很常见,因为调用方需要聚焦输入框,但不应该知道组件内部结构。
五、useImperativeHandle 限制父组件能做什么
如果直接把内部 DOM 节点暴露给父组件,父组件可以调用太多 DOM 方法,封装性会变差。
useImperativeHandle 可以只暴露你允许的命令式方法。
例如只暴露 focus 和 clear。
type InputHandle = {
focus: () => void;
clear: () => void;
};
const FancyInput = forwardRef<InputHandle>((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
clear: () => {
if (inputRef.current) inputRef.current.value = '';
},
}));
return <input ref={inputRef} />;
});
这样父组件拿到的是 InputHandle,不是完整 DOM 节点。
这能减少外部对内部实现的依赖。
六、ref 是命令式能力,使用要有边界
React 的主模型是声明式:状态变化驱动 UI。 ref 是命令式出口,适合处理焦点、滚动、动画、第三方 DOM 库集成。 如果用 ref 绕过状态管理,组件会越来越难预测。
适合 ref:
focus、scroll、measure、timer id、previous value
不适合 ref:
表单展示值、列表数据、是否显示弹窗
记忆钩子:state 改了要重画,ref 改了只记账;forwardRef 开门,useImperativeHandle 决定门里给什么。
回答这题时,最好同时讲“能做什么”和“不该做什么”。 这比只背 API 名字更像真实工程经验。
七、常见误区与追问
- 误区:ref 更新会触发组件重新渲染。
ref.current改变不会触发渲染,UI 依赖的数据应放 state。 - 误区:forwardRef 会自动暴露子组件所有内容。 它只是让子组件接收 ref,具体挂到哪个节点由子组件决定。
- 误区:useImperativeHandle 是常规通信方式。 它是命令式逃生口,普通父子通信仍优先用 props 和事件回调。
- 追问:为什么函数组件默认不能接收 ref? 因为函数组件没有实例,React 不知道 ref 应该指向哪个内部节点。
- 追问:什么时候必须用 ref? 聚焦、滚动、测量 DOM、控制媒体播放、接入非 React 插件时常用。
- 追问:useRef 和 createRef 有什么区别? 函数组件中
useRef跨渲染保持同一对象,createRef每次调用会创建新 ref。
八、加强记忆
这题按“三件套”来记:useRef 保存不触发渲染的可变引用,forwardRef 把父 ref 交给子组件,useImperativeHandle 定制暴露给父组件的方法。最后一定强调 ref 不是 state,不能用它承载需要驱动视图的数据。