React useRef 完全指南:不只是 DOM 引用
引言
在 React 开发中,我们经常听到 useState,但它的兄弟 useRef 却常被忽视。今天我们来深入探讨 useRef 的奥秘,你会发现它远比想象中强大!
一、useRef 基础概念
1.1 什么是 useRef?
useRef 是一个 React Hook,它返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数。
const refContainer = useRef(initialValue);
核心特点:
- 返回的对象在组件的整个生命周期内保持不变
- 修改
.current属性不会触发组件重新渲染 - 常用于访问 DOM 节点或存储可变值
二、useRef 的三大应用场景
2.1 场景一:访问 DOM 元素
这是 useRef 最常见的用法,类似于 Vue 中的 ref。
import { useRef, useEffect } from 'react';
function App() {
const inputRef = useRef(null); // 初始值 null
useEffect(() => {
console.log(inputRef.current); // 获取到 input DOM 节点
inputRef.current?.focus(); // 自动聚焦
}, []);
console.log(inputRef.current); // 这里输出 null(因为渲染阶段 DOM 还未创建)
return (
<>
<input ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>
聚焦输入框
</button>
</>
);
}
执行时机解析:
- 首次渲染时,
inputRef.current为null - React 将
<input>的 DOM 节点赋值给inputRef.current useEffect在 DOM 挂载后执行,此时可以访问到节点
2.2 场景二:存储可变值(防止闭包陷阱)
这是 useRef 的隐藏技能!它可以存储组件生命周期内需要持久化的值。
import { useState, useRef, useEffect } from 'react';
function Timer() {
const [count, setCount] = useState(0);
const intervalId = useRef(null);
function start() {
// 使用 useRef 存储 interval ID
intervalId.current = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
console.log('Interval ID:', intervalId.current);
}
function stop() {
// 通过 .current 访问存储的值
clearInterval(intervalId.current);
}
// 清理函数,防止内存泄漏
useEffect(() => {
return () => {
if (intervalId.current) {
clearInterval(intervalId.current);
}
};
}, []);
return (
<>
<p>计时器: {count} 秒</p>
<button onClick={start}>开始</button>
<button onClick={stop}>停止</button>
</>
);
}
2.3 场景三:解决闭包问题
看看这个常见的闭包陷阱:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
// 这里总是拿到初始的 count 值(闭包问题!)
console.log('闭包中的 count:', count);
setCount(count + 1); // 总是从 0 加到 1
}, 1000);
return () => clearInterval(timer);
}, []); // 依赖数组为空,effect 只运行一次
return <div>Count: {count}</div>;
}
使用 useRef 修复:
function CounterFixed() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
// 同步最新值到 ref
useEffect(() => {
countRef.current = count;
}, [count]);
useEffect(() => {
const timer = setInterval(() => {
// 通过 ref 获取最新值
console.log('最新 count:', countRef.current);
setCount(countRef.current + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
return <div>Count: {count}</div>;
}
三、useRef 与 useState 的深度对比
3.1 核心差异
| 特性 | useState | useRef |
|---|---|---|
| 重新渲染 | ✅ 状态变化触发重新渲染 | ❌ 修改 .current 不触发渲染 |
| 返回值 | [state, setState] 数组 | { current: value } 对象 |
| 数据持久化 | 跨渲染保持 | 跨渲染保持 |
| 使用场景 | UI 状态管理 | DOM 引用/可变值存储 |
| 同步性 | 异步更新 | 同步更新 |
3.2 实战对比示例
import { useState, useRef, useEffect } from 'react';
function ComparisonDemo() {
const [stateCount, setStateCount] = useState(0);
const refCount = useRef(0);
console.log('组件渲染 - stateCount:', stateCount, 'refCount:', refCount.current);
const handleStateClick = () => {
setStateCount(stateCount + 1);
console.log('state 点击后 - stateCount:', stateCount);
};
const handleRefClick = () => {
refCount.current += 1;
console.log('ref 点击后 - refCount:', refCount.current);
// 不会触发重新渲染,UI 不会更新!
};
const showRefValue = () => {
alert(`refCount 的当前值: ${refCount.current}`);
};
return (
<div>
<h3>useState 示例</h3>
<p>UI 显示: {stateCount}</p>
<button onClick={handleStateClick}>
stateCount++ (会重新渲染)
</button>
<h3>useRef 示例</h3>
<p>UI 显示: {refCount.current} (不会自动更新)</p>
<button onClick={handleRefClick}>
refCount++ (不会重新渲染)
</button>
<button onClick={showRefValue}>
显示 ref 的真实值
</button>
<button onClick={() => setStateCount(stateCount + 1)}>
强制重新渲染查看 ref 值
</button>
</div>
);
}
四、useRef 高级模式
4.1 组合使用:ref + state
function AdvancedRef() {
const [renderCount, setRenderCount] = useState(0);
const inputRef = useRef(null);
const previousValue = useRef('');
const handleInputChange = (e) => {
const value = e.target.value;
console.log('之前的值:', previousValue.current);
console.log('当前的值:', value);
// 保存当前值供下次比较
previousValue.current = value;
};
// 强制重新渲染来验证 ref 值的持久性
const forceRender = () => {
setRenderCount(prev => prev + 1);
};
return (
<div>
<p>组件渲染次数: {renderCount}</p>
<input
ref={inputRef}
onChange={handleInputChange}
placeholder="输入内容查看变化"
/>
<button onClick={() => inputRef.current?.focus()}>
聚焦输入框
</button>
<button onClick={forceRender}>
强制重新渲染
</button>
</div>
);
}
4.2 性能优化:避免不必要的渲染
function ExpensiveComponent() {
const [value, setValue] = useState('');
const renderCount = useRef(0);
renderCount.current += 1;
// 模拟昂贵的计算
const expensiveCalculation = () => {
console.log('执行昂贵计算...');
return value.toUpperCase();
};
return (
<div>
<p>组件渲染次数: {renderCount.current}</p>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<p>计算结果: {expensiveCalculation()}</p>
</div>
);
}
五、最佳实践与注意事项
5.1 使用建议
-
何时使用 useRef:
- 访问 DOM 节点
- 存储定时器 ID、动画帧 ID
- 保存前一次渲染的值
- 缓存昂贵的计算结果
-
何时避免 useRef:
- 需要触发 UI 更新的状态(用
useState) - 需要派生状态(用
useMemo或useCallback)
- 需要触发 UI 更新的状态(用
5.2 常见误区
// ❌ 错误:在渲染中修改 ref
function WrongUsage() {
const count = useRef(0);
count.current += 1; // 每次渲染都会执行,导致不一致
return <div>Count: {count.current}</div>;
}
// ✅ 正确:在事件处理或 effect 中修改
function CorrectUsage() {
const count = useRef(0);
const handleClick = () => {
count.current += 1;
console.log('当前值:', count.current);
};
return (
<button onClick={handleClick}>
点击次数: {count.current}
</button>
);
}
六、完整示例代码
import { useState, useRef, useEffect } from 'react';
function App() {
// 1. DOM 引用示例
const inputRef = useRef(null);
// 2. 存储可变值示例
let intervalId = useRef(null);
const [count, setCount] = useState(0);
// 3. 解决闭包问题的 ref
const countRef = useRef(count);
// 同步最新状态到 ref
useEffect(() => {
countRef.current = count;
}, [count]);
function start() {
// 使用 ref 存储定时器 ID
intervalId.current = setInterval(() => {
console.log("定时器运行中...");
setCount(prev => prev + 1);
}, 1000);
console.log("定时器 ID:", intervalId.current);
}
function stop() {
clearInterval(intervalId.current);
console.log("定时器停止");
}
// 组件挂载后自动聚焦输入框
useEffect(() => {
console.log("DOM 已挂载,inputRef:", inputRef.current);
inputRef.current?.focus();
// 清理函数
return () => {
if (intervalId.current) {
clearInterval(intervalId.current);
}
};
}, []);
// 监听 count 变化
useEffect(() => {
console.log("effect 执行,当前 count:", count);
console.log("通过 ref 获取的 count:", countRef.current);
}, [count]);
console.log("渲染阶段,inputRef:", inputRef.current);
return (
<div style={{ padding: '20px' }}>
<h1>useRef 完全指南</h1>
<section>
<h2>1. DOM 引用</h2>
<input
ref={inputRef}
placeholder="自动获得焦点"
style={{ marginRight: '10px' }}
/>
<button onClick={() => inputRef.current?.focus()}>
重新聚焦
</button>
</section>
<section style={{ marginTop: '30px' }}>
<h2>2. 存储可变值(定时器)</h2>
<p>计数: {count}</p>
<button type="button" onClick={start} style={{ marginRight: '10px' }}>
开始定时器
</button>
<button type="button" onClick={stop} style={{ marginRight: '10px' }}>
停止定时器
</button>
<button type="button" onClick={() => setCount(count + 1)}>
count++
</button>
</section>
<section style={{ marginTop: '30px' }}>
<h2>3. 闭包问题演示</h2>
<p>尝试在定时器中直接使用 count 会有闭包问题</p>
<p>使用 countRef 可以获取最新值</p>
</section>
</div>
);
}
export default App;
总结
useRef 是 React 中一个强大但常被低估的 Hook。它不仅是访问 DOM 的工具,更是:
- 状态管理的补充:存储不需要触发渲染的可变值
- 性能优化的助手:避免不必要的重新渲染
- 闭包问题的解药:在回调中访问最新值
- 持久化存储的容器:在组件生命周期内保持引用
记住这个简单法则:如果变化需要反映在 UI 上,用 useState;如果不需要,考虑 useRef。
掌握 useRef 能让你的 React 代码更加高效和健壮,是进阶 React 开发的必备技能!