useRef 到底用来干什么?不止获取 DOM 元素
如果你刚接触 React Hooks,useRef 给你的第一印象大概率是: "用来拿 DOM 的" 。
比如:
ini
const inputRef = useRef<HTMLInputElement>(null);
<input ref={inputRef} />
这没错,但这只是 useRef 的"表面工作"。
真正理解 useRef,要抓住一句话:
useRef 是 React 中"跨渲染周期的可变容器"。
这篇文章,我们就从"拿 DOM"开始,逐步拆开 useRef 的本质、常见误区和高级用法。
一、useRef 到底是什么?
1. 定义
r
useRef<T>(initialValue: T): { current: T }
- 返回一个可变对象
{ current: T } - 在组件的整个生命周期内保持不变
- 修改
ref.current不会触发组件重新渲染
这是它和 useState 最大的区别。
二、最基础用法:获取 DOM 元素
1. 获取 DOM
javascript
function TextInput() {
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => {
inputRef.current?.focus();
};
return (
<>
<input ref={inputRef} />
<button onClick={focusInput}>聚焦</button>
</>
);
}
2. 常见 DOM 场景
- 聚焦 / 选中文本
- 测量 DOM 尺寸
- 滚动控制
- 集成第三方 DOM 库(如 ECharts、Monaco Editor)
ini
const chartRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (chartRef.current) {
const width = chartRef.current.clientWidth;
// 初始化图表
}
}, []);
✅ DOM 操作是 useRef 的"第一职业",但不是唯一职业。
三、useRef vs useState:关键区别
这是面试和实战中最容易混淆的点。
| 特性 | useRef | useState |
|---|---|---|
| 是否触发渲染 | ❌ 不触发 | ✅ 触发 |
| 值是否跨渲染 | ✅ 是 | ✅ 是 |
| 是否"React 可控" | ❌ 不可控 | ✅ 可控 |
| 典型用途 | DOM、实例、缓存 | UI 状态 |
一个经典例子
javascript
function Counter() {
const countRef = useRef(0);
const [count, setCount] = useState(0);
const incrementRef = () => {
countRef.current++;
console.log('ref:', countRef.current);
// UI 不会更新
};
const incrementState = () => {
setCount(c => c + 1);
// UI 会更新
};
return (
<>
<p>state: {count}</p>
<button onClick={incrementRef}>Ref +1</button>
<button onClick={incrementState}>State +1</button>
</>
);
}
👉 结论:
- 需要 UI 响应 →
useState - 需要"悄悄存点东西" →
useRef
四、useRef 的隐藏技能:跨渲染保存可变值
1. 保存上一次的 props / state
ini
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
使用:
ini
const prevCount = usePrevious(count);
这在做"前后对比"时非常有用,比如动画、日志、表单脏检查。
2. 保存定时器 / 订阅实例
javascript
function Timer() {
const timerRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
timerRef.current = setInterval(() => {
console.log('tick');
}, 1000);
return () => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
};
}, []);
return <div>Timer</div>;
}
✅ 用 useRef 保存"清理对象",比 let timer 更安全。
3. 防止重复初始化(单例)
csharp
function useOnce(init: () => any) {
const ref = useRef<{ value: any }>();
if (!ref.current) {
ref.current = { value: init() };
}
return ref.current.value;
}
适合:
- 创建 SDK 实例
- 初始化 Web Worker
- 创建全局缓存对象
五、useRef 在闭包陷阱中的救场
React Hooks 的一个经典坑:闭包捕获旧值。
问题示例
scss
function Chat() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(count); // 永远是 0
}, 1000);
return () => clearInterval(id);
}, []);
return <button onClick={() => setCount(c => c + 1)}>+</button>;
}
用 useRef 解决
scss
function Chat() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
useEffect(() => {
const id = setInterval(() => {
console.log(countRef.current); // ✅ 永远是最新值
}, 1000);
return () => clearInterval(id);
}, []);
return <button onClick={() => setCount(c => c + 1)}>+</button>;
}
✅ useRef 是"逃离闭包陷阱"的常用手段。
六、useRef + 自定义 Hook = 强力组合
1. useLatest:永远拿到最新值
csharp
function useLatest<T>(value: T) {
const ref = useRef(value);
ref.current = value;
return ref;
}
2. useMountedRef:判断组件是否挂载
ini
function useMountedRef() {
const ref = useRef(false);
useEffect(() => {
ref.current = true;
return () => {
ref.current = false;
};
}, []);
return ref;
}
常用于异步请求中防止内存泄漏:
scss
if (mountedRef.current) {
setData(res);
}
七、useRef 的常见误区
❌ 误区 1:用 useRef 代替 useState
ini
// 错误:UI 不会更新
const countRef = useRef(0);
✅ 只要 UI 依赖这个值,就不能用 useRef。
❌ 误区 2:在渲染阶段修改 ref.current
ini
// ❌ 不推荐
ref.current = 123;
return <div />;
✅ 修改 ref 应放在:
useEffect- 事件处理函数
- 自定义 Hook 内部逻辑
❌ 误区 3:认为 ref 是响应式的
ref.current 变化 不会触发组件更新,这是设计如此,不是 bug。
八、一句话总结 useRef
useRef 是一个"在组件生命周期内存在、可变、不触发渲染的盒子"。
它适合:
- ✅ 获取 DOM
- ✅ 保存实例
- ✅ 缓存跨渲染的值
- ✅ 解决闭包问题
- ✅ 控制副作用生命周期
不适合:
- ❌ UI 状态
- ❌ 需要响应式的数据
九、什么时候该用 useRef?
你可以问自己三个问题:
- 这个值是否需要跨渲染存在?
- 修改它是否需要触发 UI 更新?
- 我是否只是想"存点东西",而不是"驱动 UI"?
如果答案是 "是 / 否 / 是" → 用 useRef。