“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 的区别?
| 特性 | useRef | useState |
|---|---|---|
| 是否触发组件更新 | ❌ 不会 | ✅ 会 |
| 适合用途 | 存储 DOM、定时器、缓存变量 | 响应式数据驱动视图 |
| 更新方式 | ref.current = value | setState(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 在函数组件中怎么使用吗?”
建议回答思路:
- 函数组件不能直接接收 ref;
- 需要用
forwardRef进行转发; - 可以结合
useImperativeHandle暴露方法; - 典型应用场景:封装 input、video、canvas 等组件,供父组件调用;
🧾 总结一下:
| API | 作用 | 是否触发渲染 | 典型用途 |
|---|---|---|---|
useRef | 储存变量或操作 DOM | ❌ 不触发 | 记录 DOM、定时器、状态等 |
forwardRef | 子组件接收 ref | ✅ 转发 | 让父组件获取子组件 DOM |
useImperativeHandle | 定制暴露的方法 | ❌ 不影响 UI | 暴露组件方法给父组件 |
✅ 最后一波彩蛋总结
🎉 useRef:
“我什么都能记,但我从不打扰 React 视图。”
🎉 forwardRef:
“你有 ref 我有 ref,我给你转发到里面的 input!”
🎉 useImperativeHandle:
“不给你全部 DOM,我只给你能用的方法(接口封装)。”
📌 写在最后
掌握 useRef + forwardRef + useImperativeHandle,你就能在组件之间写出更清晰的“遥控逻辑”和“封装接口”了。
👉 想要组件既封装又可控?用这三位组合拳就对了!