React 中 useState 与 useRef 的深度结合使用指南
·
在 React 开发中,useState 和 useRef 是我们最常用的两个 Hook。
大部分人会把它们单独理解:
useState—— 驱动组件更新的状态存储器useRef—— 存储不会触发更新的可变引用
但在实际项目里,这两个 Hook 常常需要结合起来使用,才能同时满足“响应式更新”和“高性能引用”的双重需求。本文将从基础到进阶,带你理解 useState + useRef 的最佳实践。
一、基础回顾
useState
const [count, setCount] = useState(0);
setCount(count + 1); // 更新 state 会触发重新渲染
特点:
- 值改变会导致组件重新渲染。
- 适合用来存放需要影响 UI 的数据。
useRef
const timer = useRef<number | null>(null);
useEffect(() => {
timer.current = window.setInterval(() => {
console.log("tick");
}, 1000);
return () => clearInterval(timer.current!);
}, []);
特点:
.current的值改变不会触发重新渲染。- 常用于保存 DOM 节点、定时器 ID、外部库实例等。
二、常见问题:单独使用时的缺陷
三、useState + useRef 的典型结合模式
1. 状态和快照并存
import { useState, useRef } from "react";
function InputLogger() {
const [text, setText] = useState(""); // UI 驱动
const latestText = useRef(text); // 快照缓存
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setText(e.target.value);
latestText.current = e.target.value; // 保存最新值,不触发额外渲染
};
const logLatest = () => {
console.log("最新输入(不依赖渲染):", latestText.current);
};
return (
<div>
<input value={text} onChange={handleChange} />
<button onClick={logLatest}>打印最新输入</button>
</div>
);
}
2. 避免闭包陷阱
function TimerCounter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
countRef.current = count; // 每次渲染时同步最新值
}, [count]);
useEffect(() => {
const timer = setInterval(() => {
setCount(countRef.current + 1); // 避免闭包问题
}, 1000);
return () => clearInterval(timer);
}, []);
return <h1>{count}</h1>;
}
流程示意图:
四、设计模式总结
| 场景 | 推荐 Hook | 原因 |
|---|---|---|
| 驱动 UI 更新 | useState |
渲染绑定 |
| 缓存不影响渲染的数据 | useRef |
性能优化 |
| 异步/闭包中保持最新值 | useState + useRef |
避免 stale closure |
| 存储第三方库实例、DOM 节点 | useRef |
生命周期稳定 |
| 既要 UI 更新又要缓存 | useState + useRef |
状态+快照模式 |
五、真实业务案例:输入框防抖
我们来模拟一个常见的业务场景:
一个输入框,每次输入都需要保存最新值。
- 方案 A:仅用 useState
- 每次输入都 setState,组件频繁 re-render。
- 方案 B:仅用 useRef
- 性能很好,但 UI 不会自动更新。
- 方案 C:useState + useRef 结合
- UI 更新频率受控,缓存值随时可用。
性能实验代码(简化版)
function PerformanceTest() {
const [text, setText] = useState(""); // UI 绑定
const refText = useRef(""); // 快照缓存
const renderCount = useRef(0); // 渲染计数器
renderCount.current++;
const handleChangeState = (e: React.ChangeEvent<HTMLInputElement>) => {
setText(e.target.value); // 方案 A
};
const handleChangeRef = (e: React.ChangeEvent<HTMLInputElement>) => {
refText.current = e.target.value; // 方案 B
};
const handleChangeHybrid = (e: React.ChangeEvent<HTMLInputElement>) => {
refText.current = e.target.value; // 缓存
if (e.target.value.length % 5 === 0) {
setText(refText.current); // 方案 C,控制更新频率
}
};
return (
<div>
<p>渲染次数:{renderCount.current}</p>
<input onChange={handleChangeState} placeholder="仅 useState" />
<input onChange={handleChangeRef} placeholder="仅 useRef" />
<input onChange={handleChangeHybrid} placeholder="useState + useRef" />
</div>
);
}
对比结果
假设用户输入 100 次字符:
| 方案 | 渲染次数 | UI 是否实时更新 | 性能表现 |
|---|---|---|---|
| 仅 useState | 100 次 | ✅ 是 | ❌ 低(频繁 re-render) |
| 仅 useRef | 1 次(初始) | ❌ 否 | ✅ 高(但 UI 不更新) |
| useState + useRef | 20 次(按需触发) | ✅ 是 | ⚡ 平衡(最佳实践) |
六、整体思维导图
七、结语
useState和useRef并不是对立的,而是互补的。- useState 驱动视图,useRef 保存快照,两者结合能覆盖更多场景。
- 在实际业务里:
- UI 驱动用
useState。 - 缓存 & 性能优化用
useRef。 - 异步/闭包用 二者结合。
- UI 驱动用
理解好它们的配合方式,你的 React 代码会更高效、更优雅。 🚀
更多推荐

所有评论(0)