useState 是 React 中最基础也是使用频率最高的 Hook。它让函数组件拥有了状态管理能力——组件可以记住数据,并在数据变化时自动重新渲染界面。掌握 useState,是理解整个 React 状态管理机制的起点。
基本用法:声明、读取、更新
useState 接收一个初始值,返回一个包含两个元素的数组:当前状态值 和一个更新该状态的函数。
import { useState } from 'react';
function Counter() {
// 声明状态 count,初始值为 0
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1); // 更新状态
};
return (
<div>
<p>当前计数:{count}</p>
<button onClick={handleClick}>+1</button>
</div>
);
}
count:当前状态值,渲染时使用。setCount:更新函数,调用后会触发组件重新渲染,传入的新值会替换旧值。0:初始值,只在组件首次渲染时使用。
函数式更新:安全地基于旧状态计算新状态
上面的 setCount(count + 1) 在大多数场景下工作正常,但当多次连续更新或更新依赖旧状态时,直接传值可能会引发问题。React 的“函数式更新”可以解决这个问题:传递给 setState 一个函数,该函数接收旧状态作为参数,返回新状态。
// 正确:使用函数式更新
const handleClick = () => {
setCount(prevCount => prevCount + 1);
};
为什么需要函数式更新?考虑以下场景:你在同一个事件处理函数中连续调用两次 setCount(count + 1):
function handleDoubleClick() {
setCount(count + 1); // 期望 count + 1
setCount(count + 1); // 期望再 +1,结果只加了一次
}
因为两次更新中 count 变量闭包住的是同一个旧值(0),所以两次都是 0 + 1,最终结果 count 为 1,而非 2。React 的批量更新机制会合并这两次更新。
使用函数式更新可确保每次都是基于最新状态计算:
function handleDoubleClick() {
setCount(prev => prev + 1); // prev 是 0 → 1
setCount(prev => prev + 1); // prev 是 1 → 2
}
函数式更新的核心价值在于:当你无法确定状态在“触发更新时”和“实际应用更新时”是否一致时,用函数形式可以保证准确性。这常见于:
- 快速连续点击
useEffect中的异步操作更新状态- 状态依赖多个前置因素
惰性初始化:避免重复计算初始值
如果初始状态需要经过复杂的计算(如读取 localStorage、大量数据处理),直接传给 useState 会导致每次渲染都执行这个计算,但只有首次渲染需要使用其结果,后续渲染会忽略它。这时可以传入一个函数来实现惰性初始化:
const [count, setCount] = useState(() => {
const saved = localStorage.getItem('count');
return saved ? Number(saved) : 0;
});
React 只会在组件首次渲染时调用该函数,后续渲染不再执行。这可以显著节省性能,尤其是计算昂贵的场景。
状态更新的不可变性:对象与数组的正确更新方式
React 内部依赖不可变数据来判断状态是否变化:如果状态地址未变(oldState === newState),React 会跳过重渲染。因此,更新对象或数组状态时,必须创建一份新拷贝,而不是直接修改原对象。
对象更新示例:
const [user, setUser] = useState({ name: 'Alice', age: 25 });
// ❌ 错误:直接修改原对象
user.age = 26;
setUser(user); // 不会触发重渲染
// ✅ 正确:创建新对象
setUser({ ...user, age: 26 });
setUser(prev => ({ ...prev, age: prev.age + 1 })); // 函数式更新 + 不可变
数组更新示例:
const [list, setList] = useState([1, 2, 3]);
// ❌ 错误:直接 push
list.push(4);
setList(list); // 引用没变,React 认为无变化
// ✅ 正确:返回新数组
setList([...list, 4]);
setList(prev => [...prev, 4]);
// 删除/修改同理
setList(prev => prev.filter(item => item !== 2)); // 过滤返回新数组
setList(prev => prev.map(item => item === 2 ? 20 : item)); // 修改返回新数组
使用扩展运算符、map、filter、reduce 等工具来创建新引用,是 React 开发的基本功。
状态更新是异步的:批量更新与同步表现
在 React 18 及以后,无论是事件处理函数还是 setTimeout、Promise 回调,setState 调用都是异步批量处理的。这意味着:调用 setState 后立即读取该状态变量,仍然是旧值。
const handleClick = () => {
setCount(count + 1);
console.log(count); // 仍然是旧值,因为状态更新还未被应用
};
如果需要基于新状态执行操作,应使用 useEffect 监听状态变化,或者使用函数式更新加回调(但 React 本身不提供类似 setState 的回调参数,可以使用 useEffect + 依赖项实现):
useEffect(() => {
// count 已经是最新值
localStorage.setItem('count', count);
}, [count]);
使用原则与常见坑点总结
- 只在顶层调用:不要在循环、条件或嵌套函数中调用
useState,保证每次渲染 Hook 的调用顺序一致。 - 状态最小化:只存储组件渲染需要的数据,计算出的派生数据直接在渲染时计算,或使用
useMemo。 - 多个独立状态 vs 一个对象状态:当不同部分的更新互不相关,建议拆分成多个
useState,便于独立更新避免不必要的合并开销。 - 避免闭包陷阱:在
useEffect或异步回调中读取状态,注意依赖项确保拿到最新值;或使用函数式更新避免依赖旧值。
原理小窥:Hooks 链表与更新队列
每个函数组件的 Hooks 状态被存储在组件对应的 Fiber 节点 的链表中。useState 按调用顺序在链表中占据一个“格子”,其中保存了当前状态和一个更新队列。
调用 setState 时,React 会将一次更新(新值或更新函数)加入该 Hooks 的更新队列。在下一次渲染时,React 会按顺序消费队列中的每个更新,最终计算出新的状态,并触发重渲染。函数式更新函数正是在这个消费过程中被调用的,所以能拿到最新的前一个状态值。
这种设计决定了 Hooks 必须在组件的顶层顺序调用,否则链表顺序错乱会导致状态错乱。理解这一点,能帮你更深刻地掌握 useState 的行为。
useState 是 React 组件动态性的起点,配合函数式更新、不可变数据和正确的设计模式,你可以构建出健壮且可预测的交互逻辑。