React 中 useRef 不只是拿 DOM 还能存可变值
React 中 useRef 不只是拿 DOM 还能存可变值
许多 React 开发者初识 useRef 时,只把它当作获取 DOM 元素的快捷方式。但 useRef 的真正威力远不止于此——它可以在组件的整个生命周期内 持久化存储任意可变值,且改变其内容时不会触发组件重渲染。这篇教程将带你深入这一用法,从核心机制到实战技巧,让你彻底掌握 useRef 的进阶能力。
useRef 基础回顾
在 React 中,useRef 返回一个可变的 ref 对象,该对象仅有一个 .current 属性,你可以把任何值赋给它。
import { useRef } from 'react';
function MyComponent() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return (
<>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>聚焦输入框</button>
</>
);
}
这是最经典的用法:通过 ref 属性绑定到 DOM 元素,然后通过 ref.current 访问该原生节点。
useRef 和 createRef 的关键区别在于:useRef 在函数组件每次渲染时返回的都是 同一个对象引用,而 createRef 每次渲染都会创建一个全新的 ref 对象。因此 useRef 能在多次渲染间保持数据不变,这为存储可变值打下了基础。
useRef 存储可变值的核心理念
useRef 存储的值发生改变时,不会触发组件重渲染。这与 useState 形成鲜明对比:修改 state 必然导致组件重新渲染,而修改 ref.current 则完全悄悄进行。
// 使用 state —— 每次点击都会渲染
const [count, setCount] = useState(0);
setCount(count + 1); // 触发渲染
// 使用 ref —— 点击后数值会变,但界面不刷新
const countRef = useRef(0);
countRef.current += 1; // 不触发渲染
这个特性让 useRef 成为存放那些 不影响视图输出、但需要在多次渲染间保持引用 的值的完美容器。比如:
- 存储不需要驱动 UI 变化的数据(如定时器 ID、WebSocket 实例)
- 记录某些状态的历史快照
- 作为“实例变量”在回调中共享可变数据,绕过闭包陷阱
useRef 存储可变值的典型场景
保存定时器 ID 或任何外部实例
当使用 setInterval 或 setTimeout 时,我们需要在组件卸载时清除它们。如果用 useState 存储 timer ID,每次赋值都会引起不必要的渲染,此时 useRef 是更优选择。
import { useRef, useEffect } from 'react';
function Timer() {
const intervalRef = useRef(null);
useEffect(() => {
intervalRef.current = setInterval(() => {
console.log('tick');
}, 1000);
return () => {
clearInterval(intervalRef.current);
};
}, []);
return <div>计时器运行中</div>;
}
其他如 WebSocket 连接、图表实例、第三方库对象等都可如此处理,避免将这些实例混入状态树。
保存前一个状态或属性(实现 usePrevious)
Hook 没有实例属性,useRef 可以用来手动实现类似“前一个状态”的功能。
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// 在组件中使用
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>当前值: {count}</p>
<p>上一次的值: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
注意这里 useEffect 会在渲染后更新 ref,所以 prevCount 总是显示“上一次渲染时的值”。这是一个非常经典的自定义 Hook。
记录组件是否已挂载(避免内存泄漏)
在异步操作(如网络请求)完成后,如果组件已经卸载,再去调用 setState 会导致 React 抛出警告。使用 useRef 存储一个 isMounted 标记可以优雅地解决这个问题。
import { useRef, useEffect, useState } from 'react';
function FetchData() {
const [data, setData] = useState(null);
const isMountedRef = useRef(true);
useEffect(() => {
// 组件挂载时设为 true
isMountedRef.current = true;
fetch('/api/data')
.then(res => res.json())
.then(data => {
if (isMountedRef.current) {
setData(data);
}
});
// 组件卸载时设为 false
return () => {
isMountedRef.current = false;
};
}, []);
return <div>{data ? JSON.stringify(data) : '加载中...'}</div>;
}
随着 React 18 的并发特性普及,这种模式更推荐使用 AbortController 取消请求,但 isMounted 模式在仍有很多遗留场景中十分有用。
存储不需要参与渲染的计算结果缓存
有时我们需要在组件中缓存一些计算密集型的结果,但这些结果又不需要驱动 UI 更新。useRef 能充当一个简易的缓存容器。
function ExpensiveComponent({ data }) {
const cacheRef = useRef({});
const computeResult = (data) => {
// 假设这是一个很耗时的计算
return data.reduce((acc, cur) => acc + cur, 0);
};
const result = useMemo(() => {
// 优先从缓存中读取
if (cacheRef.current.data === data) {
return cacheRef.current.result;
}
const res = computeResult(data);
cacheRef.current = { data, result: res };
return res;
}, [data]);
return <div>结果: {result}</div>;
}
虽然 useMemo 本身也可以缓存,但 useRef 给了我们手动控制缓存何时失效的能力,并且可以跨渲染保存更复杂的缓存结构。
在回调中访问最新值而无需依赖(结合 useCallback)
这是一个较少人知道但极其实用的技巧。当我们需要在 useCallback 包裹的回调中访问最新的 state 或 props,但又不想因为添加依赖而导致回调频繁重建时,可以借助 useRef 保存最新值。
function Chat({ userId }) {
const [text, setText] = useState('');
const textRef = useRef(text);
// 让 ref 一直和最新的 text 同步
useEffect(() => {
textRef.current = text;
});
// 发送消息的回调永远引用同一个函数,但能通过 textRef 获取最新 text
const sendMessage = useCallback(() => {
const currentText = textRef.current;
// 发送 currentText 到服务器
console.log('发送:', currentText, '给用户', userId);
}, [userId]); // 这里不需要 text 依赖
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<button onClick={sendMessage}>发送</button>
</div>
);
}
这样做的好处是 sendMessage 不会因为 text 变化而重新创建,从而避免不必要的子组件重渲染(如果它作为 prop 传入)。这种“可变 ref”模式在性能优化中非常实用。
使用 useRef 存储可变值的注意事项
-
更新 ref 是同步的,但视图不会自动更新
这意味着如果你偶尔想根据 ref 的值展示 UI,必须手动触发一次渲染(比如联合一个useState强制刷新),或者改用 state。 -
不要在渲染期间读写 ref
遵循 React 规范,渲染函数应当是纯函数。读取写入 ref 建议在事件处理函数或useEffect中操作,以免出现不可预期的副作用。 -
ref 的变更不会触发
useEffect重新执行
因为 React 不会追踪 ref 的变化,依赖数组中如果包含 ref,无法作为钩子的触发条件。 -
不要过度使用
如果某些数据应该是响应式的,会直接影响 UI,请使用useState或useReducer。useRef只适合那些“幕后”数据。
与 useMemo、useCallback 的配合
useRef 经常与这两个性能优化 Hook 协同工作:
useMemo帮你缓存某个计算结果,而useRef可以帮你检查输入是否真的变化,从而跳过重复计算。useCallback帮你稳定函数引用,而搭配useRef能让回调在不添加依赖的情况下访问最新状态,打破闭包陷阱。
这样的组合在一些复杂组件(例如集成第三方库、需要稳定回调的拖拽或画布操作)中几乎是必备技能。
实战:构建一个带节流的搜索框
下面我们实现一个搜索框,要求:
- 用户输入停止 500ms 后才发起搜索
- 搜索过程中不阻塞用户继续输入
- 避免不必要的 API 调用
我们将用到 useRef 存储定时器 ID,并结合 useState 和 useEffect。
import { useState, useEffect, useRef } from 'react';
function DebouncedSearch() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const timerRef = useRef(null);
useEffect(() => {
// 清除上一次的定时器
if (timerRef.current) {
clearTimeout(timerRef.current);
}
// 设置新的定时器
timerRef.current = setTimeout(() => {
if (query.trim()) {
fetch(`/api/search?q=${query}`)
.then(res => res.json())
.then(data => setResults(data));
}
}, 500);
// 清理函数:组件卸载时清除定时器
return () => {
clearTimeout(timerRef.current);
};
}, [query]);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="搜索..."
/>
<ul>
{results.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
这里 timerRef 完美存储了定时器的 ID,避免了定时器 ID 被 state 管理而触发多余渲染,同时清理函数也可靠地处理了组件卸载时的资源释放。
总结
useRef除了绑定 DOM,更重要的能力是 在组件的整个生命周期内存储任意可变值。- 修改
ref.current不会导致重渲染,适合存放与 UI 无关的数据。 - 常见场景包括:保存定时器、实现
usePrevious、记录挂载状态、缓存计算结果、稳定回调中的数据引用等。 - 与
useState正确分工:能用 state 解决的不要用 ref,需要“偷偷”存值且不影响渲染的场合用 ref。
掌握了这些模式后,你会发现 useRef 是解决许多 React 组件边界问题的利器,也是编写高性能、易维护 Hook 的基础。
常见问题
Q:我可以直接让 useEffect 依赖于 ref 吗?
A:不可以。React 不会追踪 ref.current 的变化,所以即使它变了,useEffect 也不会重新运行。如果你需要在 ref 变化时执行某些操作,通常需要搭配一个 useState 来控制触发。
Q:useRef 适合存储在线状态吗?比如用户是否在线?
A:如果这个状态不影响 UI,可以使用 ref。但通常在线状态会影响界面展示(如头像旁的小绿点),所以应该用 state。如果你只是想在事件处理中知道当前在线状态而不刷新 UI,那么 ref 更合适。
Q:useRef 和类组件中的实例属性(this.xxx)有什么区别?
A:概念上非常类似,都是在组件整个生命周期中保存可变数据且不触发渲染。useRef 是函数组件中的等价方案。
Q:使用 useRef 保存可变值有什么性能优势吗?
A:避免了不必要的渲染是其最大优势。此外,在一些需要稳定引用的场景(如传递给 useCallback 的回调),合理使用 ref 可以减少子组件的重渲染次数,从而提升整体性能。