作为 React 新手,刚接触 Hooks 时,很容易混淆 useState 和 useRef——两者都能存储数据,那到底该怎么选?
我最近通过一个「点击计数」的简单练习,结合一个关键疑问,彻底理清了它们的核心区别。这篇文章就把我的学习过程和理解分享出来,希望能帮到同样困惑的小伙伴。
一、先从一个极简练习示例入手
我最初的需求很简单:做一个点击计数组件,需要满足两个功能:
- 显示「当前轮次点击数」,支持重置;
- 显示「页面加载后的总点击数」,重置当前轮次后总次数不清零。
结合 useState 和 useRef,最终写出了这样的代码(可直接运行):
import { useState, useRef } from 'react';
function ClickCounter() {
// useState:存储当前轮次点击数(驱动视图更新)
const [currentCount, setCurrentCount] = useState(0);
// useRef:存储总点击数(持久化保存,修改不触发重渲染)
const totalCountRef = useRef(0);
// 点击计数:同时更新两个数据
const handleClick = () => {
// 更新当前轮次计数(触发重渲染)
setCurrentCount(prev => prev + 1);
// 更新总计数(直接修改 current,不触发重渲染)
totalCountRef.current += 1;
// 控制台打印总计数,验证数据
console.log('总点击数(ref):', totalCountRef.current);
};
// 重置当前轮次计数(总计数不变)
const handleResetCurrent = () => {
setCurrentCount(0);
};
return (
<div style={点击计数练习当前轮次点击数:{currentCount}总点击数(持久化):{totalCountRef.current}<button onClick={ }}>
点击计数
<button onClick={handleResetCurrent}>
重置当前计数
);
}
export default function App() {
return <ClickCounter />;
}
二、我的核心疑问:useRef 不是不能驱动视图吗?为什么能写在 JSX 里?
写完代码后,我立刻产生了一个疑问:
之前学习时知道
useRef用于存储不需要驱动视图更新的数据,修改ref.current不会触发重渲染。但这个示例里,我把totalCountRef.current直接写在了 JSX 里,这难道不矛盾吗?这样写到底有没有问题?
这个疑问也让我意识到,很多新手对 useState 和 useRef 的理解,可能都停留在「表面定义」,没有深入到「渲染逻辑」层面。
三、彻底搞懂:useRef 写在 JSX 里到底行不行?
带着疑问,我梳理了核心结论,再通过代码执行过程验证,终于彻底明白:
1. 语法上:完全没问题
React 的 JSX 支持嵌入任意合法的 JavaScript 表达式,totalCountRef.current 本质就是一个普通的 JS 变量(或数值、字符串等),所以把它写在 JSX 里不会报错,属于合法写法。
2. 功能上:能显示但不能「主动驱动更新」
这是最关键的一点——useRef 的值可以显示在页面上,但修改它不会触发组件重渲染,因此页面上的显示值不会「实时更新」,只会显示组件上一次渲染时的旧值。
我们结合示例代码的执行过程,一步步拆解:
步骤 1:组件首次渲染
currentCount = 0(useState初始值);totalCountRef.current = 0(useRef初始值);- JSX 渲染结果:当前轮次点击数:0,总点击数:0。
步骤 2:第一次点击「计数」按钮
- 执行
setCurrentCount(prev => prev + 1):修改useState状态,触发组件重渲染; - 执行
totalCountRef.current += 1:totalCountRef.current变成 1,但这个修改不会触发重渲染; - 因为
useState触发了重渲染,JSX 会重新读取所有值,此时总点击数显示为 1(这里能更新是因为 useState 带动了渲染,不是 useRef 自己驱动的)。
步骤 3:验证「只改 useRef 不触发更新」
为了更直观验证,我加了一个「只修改总计数」的按钮:
<button onClick={ totalCountRef.current += 1}>只改总计数
点击这个按钮后:
- 控制台打印
totalCountRef.current,会发现数值确实增加了; - 但页面上的总点击数没有任何变化——因为没有修改
useState状态,组件没有重渲染,JSX 里的totalCountRef.current还是上一次渲染的旧值。
四、useState 与 useRef 的核心差异总结
通过这个示例和疑问,我终于理清了两者的核心区别,用表格总结最清晰:
| 对比维度 | useState | useRef |
|---|---|---|
| 核心作用 | 存储驱动视图更新的状态 | 存储持久化、不驱动视图的数据 |
| 修改后是否触发重渲染 | 是(调用 setXxx 必触发) | 否(直接改 current 不触发) |
| 数据更新方式 | 不可变更新(必须用 setXxx) | 可变更新(直接改 current) |
| 写在 JSX 里的效果 | 实时更新显示 | 不主动更新,需依赖其他逻辑触发渲染 |
| 适用场景 | 页面需要实时显示的数据(计数、表单值、列表等) | 定时器 ID、DOM 元素引用、持久化不显示的状态(如总点击数) |
五、新手避坑指南
梳理完这些内容后,我总结了几个新手容易踩的坑,供大家参考:
1. 不要用 useRef 存储需要实时显示的数据
如果数据需要在页面上实时更新(比如当前轮次点击数),一定要用 useState。用 useRef 只会导致页面显示滞后。
2. 不要误以为 useRef 不能写在 JSX 里
语法上完全可以写,但要清楚它的局限性——不能主动驱动更新。只有数据不常变化(如固定 ID、初始化后的常量)时,写在 JSX 里才合理。
3. 避免滥用「强制更新」让 useRef 实时显示
如果非要让 useRef 的值实时显示,有人会用「空 state」强制触发渲染:
const [, forceUpdate] = useState({});
const handleClick = () => {
totalCountRef.current += 1;
forceUpdate({}); // 强制触发渲染
};
这种写法虽然能实现效果,但不推荐——既然需要实时更新,直接用 useState 管理数据更符合 React 设计理念,代码也更简洁。
4. 普通变量无法替代 useRef 做持久化
新手可能会想:“既然 useRef 只是持久化数据,用普通变量不行吗?” 答案是不行——组件每次重渲染时,函数内部的普通变量都会重新初始化(比如 let totalCount = 0 会每次重置为 0),而useRef 的 current 在组件整个生命周期内都会保留数据。
六、总结
其实 useState 和 useRef 的选择逻辑很简单:
如果数据需要驱动视图更新 → 用 useState;
如果数据只需要持久保存、不需要驱动视图 → 用 useRef。
这次通过一个简单的练习示例,加上自己的疑问和梳理,让我对这两个 Hooks 的理解从「表面记忆」变成了「深层理解」。希望我的学习过程能帮到更多新手小伙伴~
如果有疑问或不同看法,欢迎在评论区交流~