useRef 详解:功能、用法与常见场景
在 React 中,useRef 是一个非常强大且多功能的 Hook,它主要用于:
- 保存可变值:在组件生命周期内保持引用不变
- 访问 DOM 元素:替代传统的
ref属性 - 缓存复杂对象:避免重复计算或渲染
下面从原理到实践,详细解析 useRef 的各种用法。
一、核心概念与基本用法
1. 基本语法
const refContainer = useRef(initialValue);
initialValue:初始值,可以是任意类型refContainer:返回的 ref 对象,包含一个可变的.current属性
2. 关键特性
- 引用不变性:组件重新渲染时,
useRef返回的始终是同一个 ref 对象 - 可变值存储:
ref.current可以随时修改,不会触发重新渲染 - 跨渲染周期共享:
ref.current的值在组件整个生命周期内保持
二、常见应用场景
1. 访问 DOM 元素
最常见的用法是获取 DOM 节点,执行聚焦、测量尺寸等操作。
示例:聚焦输入框
import { useRef } from 'react';
function TextInputWithFocusButton() {
const inputRef = useRef(null);
const handleClick = () => {
inputRef.current.focus(); // 直接访问 DOM 方法
};
return (
<>
<input ref={inputRef} type="text" />
<button onClick={handleClick}>聚焦输入框</button>
</>
);
}
示例:测量元素尺寸
function MeasureElement() {
const divRef = useRef(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const measure = () => {
if (divRef.current) {
const { width, height } = divRef.current.getBoundingClientRect();
setDimensions({ width, height });
}
};
return (
<div>
<div ref={divRef} style={{ width: '50%', margin: 'auto' }}>
测量我
</div>
<button onClick={measure}>获取尺寸</button>
<p>宽: {dimensions.width}px, 高: {dimensions.height}px</p>
</div>
);
}
2. 存储定时器或异步操作
保存定时器 ID 或其他需要清理的资源,避免内存泄漏。
示例:使用 ref 保存定时器
function Timer() {
const [count, setCount] = useState(0);
const timerRef = useRef(null);
const startTimer = () => {
timerRef.current = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
};
const stopTimer = () => {
clearInterval(timerRef.current); // 安全清除定时器
};
useEffect(() => {
return () => clearInterval(timerRef.current); // 组件卸载时清理
}, []);
return (
<div>
<p>计数: {count}</p>
<button onClick={startTimer}>开始</button>
<button onClick={stopTimer}>停止</button>
</div>
);
}
3. 缓存复杂对象
避免在每次渲染时重新创建相同的对象,尤其是大型数据结构或函数。
示例:缓存大型数组
function HeavyComponent() {
const largeArrayRef = useRef(null);
// 只在首次渲染时创建大型数组
if (!largeArrayRef.current) {
largeArrayRef.current = new Array(1000).fill(0).map((_, i) => i);
}
return (
<div>
{/* 使用 largeArrayRef.current */}
</div>
);
}
4. 存储上一次的值
在状态更新时保留之前的值,用于比较或计算差值。
示例:比较当前值与上一次的值
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef(0);
useEffect(() => {
prevCountRef.current = count; // 保存当前值到 ref
}, [count]);
const prevCount = prevCountRef.current; // 获取上一次的值
return (
<div>
<p>现在: {count}, 之前: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
);
}
5. 跨组件通信
在某些场景下,可以通过父组件传递 ref 实现跨层级通信。
示例:父组件调用子组件方法
// 子组件
function ChildComponent() {
const doSomething = () => {
console.log('子组件执行操作');
};
// 将方法挂载到 ref 上
const ref = useRef({ doSomething });
return <div ref={ref}>子组件</div>;
}
// 父组件
function ParentComponent() {
const childRef = useRef(null);
const handleClick = () => {
childRef.current?.doSomething(); // 调用子组件方法
};
return (
<div>
<ChildComponent ref={childRef} />
<button onClick={handleClick}>调用子组件方法</button>
</div>
);
}
三、useRef vs useState
| 特性 | useRef | useState |
|---|---|---|
| 触发重新渲染 | 否 | 是 |
| 值的持续性 | 整个组件生命周期 | 每次渲染可能不同(基于依赖) |
| 用途 | DOM 访问、存储临时值 | 管理驱动 UI 渲染的状态 |
| 更新方式 | 直接修改:ref.current = value | 通过 setter:setState(value) |
四、注意事项与常见误区
-
不要在渲染过程中修改
ref.current- 可能导致意外行为,因为渲染期间的修改不会反映到当前渲染中。
-
避免过度使用
useRef- 如果某个值需要触发 UI 更新,应该使用
useState或useReducer。
- 如果某个值需要触发 UI 更新,应该使用
-
清理副作用
- 如果
ref.current保存了需要清理的资源(如定时器),在useEffect中进行清理。
- 如果
-
与 forwardRef 结合使用
- 当需要将 ref 传递给子组件时,使用
React.forwardRef:
const Child = React.forwardRef((props, ref) => { return <input ref={ref} />; }); - 当需要将 ref 传递给子组件时,使用
五、总结
useRef 是 React 中一个多功能的 Hook,核心价值在于:
- 保持引用稳定性:避免因引用变化导致不必要的重渲染
- 存储可变数据:在不触发渲染的情况下保存和访问值
- DOM 操作:安全高效地访问和操作 DOM 元素
合理使用 useRef 可以解决许多性能问题和复杂场景,但需注意与 useState 的边界,确保状态管理逻辑清晰。