← 返回题目列表

useRef、forwardRef 和 useImperativeHandle 有什么区别?

高频 中等 第 15 / 27 题 更新于 2026/07/29
ReactuseRefforwardRefuseImperativeHandleref

简化版

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 可以只暴露你允许的命令式方法。 例如只暴露 focusclear

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,不能用它承载需要驱动视图的数据。