2、React中的useState:基础用法、函数式更新、惰性初始化、状态更新的异步特性
useState 是 React Hooks 里最基础、也最常用的一个。说实话,我面试前端候选人时,经常发现很多人用了两三年 useState,却连它的异步特性和惰性初始化都说不清楚。这一节,咱们就把这几个核心点彻底聊透。
2.1 基础用法:从声明到更新
useState 的签名很简单:
const [state, setState] = useState(initialValue);
它返回一个数组,第一个元素是当前状态值,第二个是更新函数。我习惯用数组解构的方式命名,语义清晰,一眼就能看出哪个是数据、哪个是更新方法。
来看一个最简单的计数器:
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>当前计数:{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
点击按钮,count 加 1,界面自动更新。嗯,就是这么直接。
2.2 函数式更新:解决「过期闭包」问题
基础用法看起来没问题,但如果你在短时间内连续调用多次 setCount,就会踩坑:
function handleClick() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}
// 结果:count 只加了 1,而不是 3
为什么会这样?因为每次 setCount 拿到的 count 都是当前渲染快照里的值,三个 setCount 读取的是同一个 count,所以最终只加了一次。
解决办法就是函数式更新:
function handleClick() {
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
}
// 结果:count 加了 3
函数式更新的参数是上一次的状态值,React 会保证这个 prev 是最新的。我在项目中遇到过好几次因为没注意这个细节导致的 bug,尤其是在处理购物车数量加减、分页器连续翻页时,函数式更新几乎是必须的。
2.3 惰性初始化:别再每次都算一遍了
useState 的初始值可以是一个值,也可以是一个函数。当初始值需要经过复杂计算才能得到时,用函数形式可以避免每次渲染都重复计算:
// ❌ 每次渲染都会执行 expensiveComputation
const [data, setData] = useState(expensiveComputation());
// ✅ 只在首次渲染时执行一次
const [data, setData] = useState(() => expensiveComputation());
这个特性叫惰性初始化(Lazy Initialization)。说白了,就是告诉 React:「这个初始值很贵,你帮我缓存一下,别每次渲染都重新算。」
我曾在项目中用这个特性初始化一个大型树形结构数据,初始计算耗时约 200ms。如果不做惰性初始化,每次组件更新都会卡 200ms,用户体验极差。改成函数形式后,只有首次挂载才计算,后续更新完全不受影响。
- 从 localStorage 读取并解析数据
- 对 props 进行复杂转换
- 生成唯一 ID 或时间戳
- 初始化大型数组或对象
2.4 状态更新的异步特性:别指望立即拿到新值
这是面试高频题,也是很多新手容易懵的地方。看这段代码:
function handleClick() {
setCount(count + 1);
console.log(count); // 打印的还是旧值
}
为什么打印的是旧值?因为 setCount 是异步的。React 不会立即更新状态,而是把更新加入队列,等到合适的时机(通常是事件处理函数执行完毕)再批量处理。
React 这么做有两个原因:
- 性能优化:批量合并多个 setState,减少不必要的渲染次数
- 一致性保证:避免在同一个事件循环中多次读取到不一致的状态
来看一个更典型的例子:
function handleClick() {
setCount(c => c + 1);
setCount(c => c + 1);
setFlag(true);
setFlag(f => !f);
}
// React 会把这四个更新合并成一次渲染
React 18 之后,所有更新默认都是自动批处理的,包括 setTimeout、Promise 回调、原生事件处理函数。这意味着你几乎不需要再手动使用 unstable_batchedUpdates 了。
2.5 知识结构总览
下面这张图把 useState 的核心知识点串了起来,方便你建立整体认知:
2.6 常见误区与最佳实践
| 误区 | 错误写法 | 正确做法 |
|---|---|---|
| 直接修改状态 | state.xxx = newValue |
使用 setState 并返回新对象 |
| 依赖旧值却不用函数式更新 | setCount(count + 1) |
setCount(prev => prev + 1) |
| 每次渲染都执行复杂初始化 | useState(expensive()) |
useState(() => expensive()) |
| 更新后立即读取新值 | setState(x); console.log(state) |
使用 useEffect 或在回调中读取 |
useState 虽然简单,但用好它需要理解 React 的更新机制。函数式更新解决闭包问题,惰性初始化优化性能,异步特性要求你改变「立即获取新值」的思维习惯。把这些基础打牢,后面学 useReducer、useContext 时会轻松很多。