useRef 是 React 提供的一个 Hook,它返回一个可变的 ref 对象,该对象在组件的整个生命周期中持续存在。最常见的两个用途是:获取 DOM 元素的引用,以及存储不触发重新渲染的可变数据。
基本语法
import { useRef } from 'react';
function MyComponent() {
const refContainer = useRef(initialValue);
// refContainer.current 持有当前值
}
useRef 接受一个初始值作为参数,并返回一个带有 current 属性的普通 JavaScript 对象。这个对象在每次渲染时都是同一个引用,修改 .current 不会触发组件重新渲染。
用途一:获取 DOM 元素的引用
当你需要在 React 中直接操作 DOM(例如聚焦输入框、测量元素尺寸、集成第三方非 React 库)时,useRef 是最标准的方式。
function AutoFocusInput() {
const inputRef = useRef(null);
useEffect(() => {
// 组件挂载后自动聚焦输入框
inputRef.current?.focus();
}, []);
return <input ref={inputRef} placeholder="自动聚焦" />;
}
关键点:
- 将 ref 对象传给 JSX 元素的
ref属性,React 会在组件挂载后将该元素的 DOM 节点赋值给ref.current。 - 组件卸载时,
ref.current自动重置为null。 - 不要试图在渲染期间读取或修改
ref.current(除非涉及 ref 回调函数等特定场景),否则可能导致行为不一致。
常用 DOM 操作示例:
function VideoPlayer() {
const videoRef = useRef(null);
const handlePlay = () => {
videoRef.current?.play();
};
const handlePause = () => {
videoRef.current?.pause();
};
return (
<div>
<video ref={videoRef} src="movie.mp4" />
<button onClick={handlePlay}>播放</button>
<button onClick={handlePause}>暂停</button>
</div>
);
}
用途二:存储跨渲染周期的可变变量
如果你需要在整个组件生命周期内保持某个可变值,又不需要它触发重新渲染,useRef 是最佳选择。典型场景包括:
- 保存定时器 ID
- 保存上一次的 props 或 state(用于比较变化)
- 记录某些副作用执行次数
- 存储不需要显示在 UI 上的“临时”数据
保存定时器 ID
function Timer() {
const [count, setCount] = useState(0);
const timerRef = useRef(null);
const startTimer = () => {
// 避免重复启动
if (timerRef.current !== null) return;
timerRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
};
const stopTimer = () => {
clearInterval(timerRef.current);
timerRef.current = null;
};
// 组件卸载时清理定时器
useEffect(() => {
return () => clearInterval(timerRef.current);
}, []);
return (
<div>
<p>计数:{count}</p>
<button onClick={startTimer}>开始</button>
<button onClick={stopTimer}>停止</button>
</div>
);
}
这里 timerRef 用来保存定时器 ID,修改它不会导致组件重新渲染,并且在多次渲染之间值保持不变。
记录上一次的 state(实现 componentDidUpdate 比较)
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(c => c + 1)}>增加</button>
</div>
);
}
useRef 保存了上一次渲染时的 count,通过比较 prevCount 和 count 可以执行特定逻辑,而无需将旧值存入 state(避免无意义的重渲染)。
存储不需要渲染的数据(例如复杂计算结果的缓存)
function ExpensiveComponent({ data }) {
const computedCache = useRef(null);
const getComputedValue = () => {
if (computedCache.current) return computedCache.current;
// 假设这是一个非常耗时的计算
const result = expensiveCalculation(data);
computedCache.current = result;
return result;
};
return <div>{getComputedValue()}</div>;
}
注意:在并发模式下,这种手动缓存可能因为意外被多次调用而产生问题,更推荐使用 useMemo 来缓存计算结果。但如果计算结果不需要依据依赖变化重新计算,或者你希望完全手动控制缓存,useRef 是一个合理的兜底方案。
useRef 与 useState 的区别
| 特性 | useRef | useState |
|------|--------|----------|
| 返回值的变更 | 修改 .current 不会触发重渲染 | 调用 setter 会触发重渲染 |
| 读取时效性 | 总是在任何时刻读取到最新值(包括在异步回调中) | 在闭包中可能捕获旧的 state(过期闭包) |
| 典型用途 | DOM 引用、定时器 ID、任意可变持久化值 | UI 状态,直接与视图绑定的数据 |
关键洞察:useRef 可以看作是 useState 的“渲染非敏感”版本。它完美解决了两个痛点:
- 需要持久化一个值,但又不想因为它的变化而重新渲染。
- 在闭包(如定时器回调、事件监听器)中需要始终读取到该值的最新内容,而不会像 state 那样受闭包陷阱影响。
常见陷阱与注意事项
1. 避免在渲染期间写入或读取 ref.current(用于 DOM 时除外)
React 的渲染应该是纯函数,如果在渲染期间修改 ref,可能与其他渲染副作用产生冲突,破坏并发渲染的安全性。正确的做法是在事件处理函数或副作用(useEffect)中操作 ref。
2. 不要用 ref 来代替 state
如果某个值的变化需要反映到 UI 上,那么它应该是 state(或派生 state)。ref 仅用于不影响视图的“幕后”数据。
3. 使用回调 ref 获取动态元素
当 ref 需要附加到条件渲染的元素上时,useRef 配合 ref 属性有时不够灵活。可以使用 callback ref 来获取变化的元素:
function MeasureExample() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<div ref={measuredRef}>
<h1>Hello, world</h1>
</div>
);
}
总结
useRef 是一个简单但功能强大的 Hook,适用于两种核心场景:
- 获取 DOM 节点:操作原生 DOM,如聚焦、滚动、动画等。
- 存储不触发渲染的可变值:保存任何需要在组件生命周期内保持可变的引用,避免闭包陷阱,同时不引入额外的渲染。
合理使用 useRef 可以让你的组件逻辑更清晰、性能更佳,避免不必要的 state 和渲染开销。