🚀从 useRef 到 forwardRef:React 中的“隐形人”和“转发小哥”全解析!

124 阅读3分钟

“ref 又不是 state,它到底是个啥?”
“forwardRef 是不是拿来操作子组件的 DOM?为什么不能直接用 ref?”
“为啥 React 总是搞些拐来拐去的 API?”

如果你也曾在这些问题里苦苦挣扎,那恭喜你点进了这篇神文,我们就来把这对 React 中的“黄金搭档”:useRef + forwardRef,一次讲清!


🎬 前言:认识一下 useRef,这位不吭声的老实人

在 React 中,useRef 是个 “哑巴 Hook”

  • 不会让组件重新渲染
  • 却能悄悄记录和存储数据
  • 还能偷偷指向真实的 DOM 元素

这就很像你有个小笔记本,啥都能记,但你一改内容,React UI 丝毫不动容——安静,稳定,可靠!


🧠 useRef 是什么?

const myRef = useRef(initialValue);

它返回一个对象:

{
  current: initialValue
}

你可以读写 myRef.current,但不会触发组件重新渲染。它就像一个不参与 UI 更新的“万能储物柜”。


🔍 典型用法一:操作 DOM(经典用法)

import { useRef } from 'react';

function MyInput() {
  const inputRef = useRef();

  return (
    <>
      <input ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>
        聚焦输入框
      </button>
    </>
  );
}

用法几乎等价于 document.querySelector(),但符合 React 数据驱动思想。


🔍 典型用法二:存储不会触发重新渲染的值

const timerId = useRef(null);

useEffect(() => {
  timerId.current = setInterval(() => {
    console.log('我是个定时器');
  }, 1000);

  return () => clearInterval(timerId.current);
}, []);

这个 timerId 不会因为更新而导致组件重渲染,是存定时器 ID 的理想选择。


🧠 useRef vs useState 的区别?

特性useRefuseState
是否触发组件更新❌ 不会✅ 会
适合用途存储 DOM、定时器、缓存变量响应式数据驱动视图
更新方式ref.current = valuesetState(value)
推荐场景不需要 UI 响应的状态UI 需要更新的状态

口诀:改了要刷新?用 state;悄悄记下来?用 ref!


🧨 然而用 ref 操作“子组件”时就 GG 了...

function Child() {
  return <input />;
}

export default function App() {
  const ref = useRef();

  return <Child ref={ref} />; // ❌ 会报错!
}

为什么?因为:

函数组件默认无法接收 ref!

这是 React 的设计,ref 默认只对 DOM 元素类组件 有效。

那我们要怎么办?难道不能操作子组件内的 input?


🧙‍♂️ forwardRef 来了:React 中的“转发小哥”

forwardRef 是 React 提供的一个 高阶组件,用于让子组件能接收到父组件传下来的 ref

const Child = React.forwardRef((props, ref) => {
  return <input ref={ref} />;
});

有了它,我们就能从父组件直接操作子组件内部的 DOM。


✅ 实战案例:父组件控制子组件 input 聚焦

const InputBox = React.forwardRef((props, ref) => {
  return <input ref={ref} />;
});

export default function App() {
  const inputRef = useRef();

  return (
    <>
      <InputBox ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>
        点我聚焦子组件
      </button>
    </>
  );
}

大功告成!现在父组件可以控制子组件中的 <input /> 了!


🎮 进阶:暴露方法给父组件(useImperativeHandle)

你不光能转发 DOM,还能自定义子组件向父组件暴露的方法

import { useRef, forwardRef, useImperativeHandle } from 'react';

const MyInput = forwardRef((props, ref) => {
  const realInput = useRef();

  useImperativeHandle(ref, () => ({
    focus: () => realInput.current.focus(),
    clear: () => (realInput.current.value = ''),
  }));

  return <input ref={realInput} />;
});

export default function App() {
  const inputRef = useRef();

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>聚焦</button>
      <button onClick={() => inputRef.current.clear()}>清空</button>
    </>
  );
}

这就好比你对孩子说:“把遥控器给我,我来操作你!”而孩子说:“好啊,这是我暴露给你的遥控功能:聚焦和清空。”


💬 面试常问:关于 ref/forwardRef 的经典题目

❓“你知道 ref 在函数组件中怎么使用吗?”

建议回答思路:

  1. 函数组件不能直接接收 ref;
  2. 需要用 forwardRef 进行转发;
  3. 可以结合 useImperativeHandle 暴露方法;
  4. 典型应用场景:封装 input、video、canvas 等组件,供父组件调用;

🧾 总结一下:

API作用是否触发渲染典型用途
useRef储存变量或操作 DOM❌ 不触发记录 DOM、定时器、状态等
forwardRef子组件接收 ref✅ 转发让父组件获取子组件 DOM
useImperativeHandle定制暴露的方法❌ 不影响 UI暴露组件方法给父组件

✅ 最后一波彩蛋总结

🎉 useRef
“我什么都能记,但我从不打扰 React 视图。”

🎉 forwardRef
“你有 ref 我有 ref,我给你转发到里面的 input!”

🎉 useImperativeHandle
“不给你全部 DOM,我只给你能用的方法(接口封装)。”


📌 写在最后

掌握 useRef + forwardRef + useImperativeHandle,你就能在组件之间写出更清晰的“遥控逻辑”和“封装接口”了。

👉 想要组件既封装又可控?用这三位组合拳就对了!