useRef / useImperativeHandle:
逃离 render 的状态、打破单向数据流的“合法后门”
核心心智模型
useRef 存的是“不参与渲染的可变引用”
这句话贯穿全文。
文章总览结构
useRef:非渲染状态的容器
useImperativeHandle:子组件主动暴露能力
它们解决的不是“数据管理”
而是:**跨 render 的引用、命令式控制**
1️. 一个你一定写过的“反直觉 Bug”
场景:计数器 + 打印最新值
import Content from "@/layout/Content";
import { Button, Input } from "antd";
import { useEffect, useRef, useState } from "react";
export default function UseRefDemo() {
const [count, setCount] = useState(0);
//const countRef = useRef(count);
const logCount = () => {
console.log(count);
};
useEffect(() => {
const timer = setInterval(() => {
console.log(count);
//console.log(countRef.current);
}, 1000);
return () => clearInterval(timer);
}, []);
return (
<Content>
<div>count = {count}</div>
<div className="m-[10px]">
<Button
type="primary"
onClick={() => {
setCount((c) => c + 1);
//countRef.current = count + 1;
}}
>
+1
</Button>
</div>
<div className="m-[10px]">
<Input
placeholder="render测试"
value={count}
width={200}
className="w-[200px]!"
/>
</div>
<div>
<Button type="primary" onClick={logCount}>
log
</Button>
</div>
</Content>
);
}
现象
- useEffect 里面得定时器永远打印得是初始值0,原因也很简单,因为useEffect并没有render
- [count],加一个依赖就可以解决这个问题
我把代码改为
console.log("Effect 执行,依赖 count =", count);
const timer = setInterval(() => {
console.log(count);
// console.log(countRef.current);
}, 1000);
return () => clearInterval(timer);
}, [count]);
然后观察发现
- 打印
Effect 执行,依赖 count = 1 index.tsx:14
Effect 执行,依赖 count = 2 3index.tsx:16 2,- useeffect里面得所有逻辑都会执行,因为[count]依赖变化
抛出我的问题,我现在不想useEffect里面得逻辑随着[count]依赖值变化,然后我想打印,拿到最新值,what can i do ?
2️.useRef:不参与渲染的“外挂状态”
useRef 是什么?
const ref = useRef(initialValue);
- ref 变了 → 组件不会 render
- render 多少次 → ref 永远是同一个对象
useRef的值变化不参与 React 的更新(render)调度,但它的创建和销毁仍受 React 生命周期管理。跳出三界之外,在五行之中😁
用 useRef 修复问题
export default function UseRefDemo() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
console.log("Effect 执行,依赖 count =", count);
const timer = setInterval(() => {
// console.log(count);
console.log(countRef.current);
}, 1000);
return () => clearInterval(timer);
}, []);
}
发生了什么?
- state 驱动 render
- console.log("Effect 执行,依赖 count =", count)并没有被打印,ref 保存“最新值”,我们只是拿到了最新引用。
- 读 ref = 读真实状态
⚠️ ref 是绕过 render 的通道
3️. useRef 的三种“正经用途”
① 访问 DOM
const inputRef = useRef<HTMLInputElement>(null);
<input ref={inputRef} />
inputRef.current?.focus();
② 保存定时器 / WebSocket / 实例
const timerRef = useRef<number | null>(null);
避免:
- 多次 render 创建多个实例
- cleanup 找不到旧引用
③ 保存“上一次的值”
const prevCount = useRef(count);
useEffect(() => {
prevCount.current = count;
});
4️. useRef vs useState
| 对比项 | useState | useRef |
|---|---|---|
| 变更是否 render | ✅ | ❌ |
| 是否参与视图 | ✅ | ❌ |
| 跨 render 持久化 | ❌ | ✅ |
| 是否触发更新链 | ✅ | ❌ |
ref.current的变化
不会触发 renderReact 不会“追踪” ref 的变化
不会进入调度 / diff / commit所以 ref 适合存放不用于驱动 UI 的数据
5️. useImperativeHandle:官方允许的“命令式后门”
问题场景
父组件想:
- focus 子组件 input
- 调用子组件方法
但不想通过 props 一路传
错误做法
把 DOM ref 直接暴露给父组件
强耦合 + 泄露实现
6️. useImperativeHandle 正确用法
子组件
import { forwardRef, useImperativeHandle, useRef } from "react";
const Child = forwardRef((props, ref) => { const inputRef = useRef(null);
useImperativeHandle(ref, () => ({ focus() { inputRef.current?.focus(); }, hello() { if (inputRef.current) { inputRef.current.value = "hello"; } }, })); console.log(inputRef.current, "value");
return ( ); });
export default Child;
父组件
import Content from "@/layout/Content";
import { useRef } from "react";
import Child from "./components";
export default function UseImperativeHandleDemo() {
const childRef = useRef<{ focus: () => void; hello: () => void }>(null);
console.log(childRef.current, "childRef.current");
return (
<Content>
<Child ref={childRef} />
<button
className="border p-2 block mt-4 bg-blue-500 text-white rounded-md"
onClick={() => childRef.current?.focus()}
>
focus child
</button>
<button
className="border p-2 block mt-4 bg-blue-500 text-white rounded-md"
onClick={() => childRef.current?.hello()}
>
hello child
</button>
</Content>
);
}
本质
useImperativeHandle = 控制 ref 暴露的能力边界
不是“暴露全部”,而是:
useImperativeHandle(ref, () => ({ focus() { inputRef.current?.focus(); }, hello() { if (inputRef.current) { inputRef.current.value = "hello"; } }, }));\
- 子组件通过useImperativeHandle暴露出自身方法,同时子组件绑定ref( ref={inputRef})
- 1.const childRef = useRef<{ focus: () => void; hello: () => void }>(null);
2.onClick={() => childRef.current?.hello()}
父组件通过ref调用子组件方法,而无需关注子组件具体代码实现
7️. useRef + useCallback:逃离闭包的组合拳
const latestValue = useRef(value);
useEffect(() => {
latestValue.current = value;
}, [value]);
const handler = useCallback(() => {
doSomething(latestValue.current);
}, []);
ref 保存最新值
callback 永远稳定
🔥 实战中非常常见
8️. 什么时候不该用?
- 想靠 ref 驱动 UI
- 用 ref 替代 state
- 用 ref 逃避数据流设计
⚠️ ref 是后门,不是主干道
9️. 最终总结
- useRef 是 不参与 render 的可变容器
- state 管 UI,ref 管“过程”
- useImperativeHandle 是 受控的命令式暴露
- ref 是 React 对“命令式编程”的妥协
🔚 面试必杀句(建议原封不动)
useRef 为什么修改不触发 render?
因为 ref 存的是可变引用,不参与 React 的更新调度,它的存在就是为了绕开 render。