讲解
普通变量记不住东西:组件函数每次渲染都重新执行,局部变量全部重置。useState 让函数组件拥有「跨渲染的记忆」:const [count, setCount] = useState(0) 返回当前值和更新函数,调用 setCount 会触发重新渲染,下一次渲染时 count 就是新值。
两个行为细节必须刻进脑子。第一,setState 是异步批处理的:setCount(count + 1); setCount(count + 1); 连续调两次,count 只加一——两次读到的是同一个旧值,React 把更新合并到下一次渲染。想基于「最新值」更新用函数式:setCount(c => c + 1),它拿到的是排队后的最新状态,调两次就加二。第二,状态是快照:每次渲染有自己的一份 state,事件回调里读到的永远是触发时那次渲染的值——不是 bug,是设计。
状态放哪、放什么也有讲究。原则一:只存「不能从其他值推导」的最小数据——全名可以从姓和名推导,就别存全名(推导放渲染时算或用 useMemo)。原则二:状态提升——两个组件要共享同一份数据,把 state 提到它们最近的共同父组件,通过 props 传下去。原则三:不相关的数据拆成多个 useState,强相关的合并成一个对象(更新时记得展开旧值)。
示例
模拟 useState 的更新队列与批处理,看清「两次 +1 为什么只加一」(在本教程构建时被真实执行):
import assert from 'node:assert/strict';
// 迷你 useState:更新进队列,渲染时统一结算
function createState(initial) {
let value = initial;
const queue = [];
return {
get value() {
return value;
},
set(update) {
// 函数式更新拿到的是队列结算后的最新值;直接给值则用当前值
queue.push(update);
},
render() {
// 一次渲染 = 依次应用队列里的所有更新
for (const u of queue) {
value = typeof u === 'function' ? u(value) : u;
}
queue.length = 0;
return value;
},
};
}
// 场景一:两次直接赋值 → 只加一(两次都是 0 + 1)
const s1 = createState(0);
s1.set(s1.value + 1); // 读到 0,排队「变 1」
s1.set(s1.value + 1); // 还是读到 0,又排队「变 1」
assert.strictEqual(s1.render(), 1);
// 场景二:两次函数式更新 → 加二
const s2 = createState(0);
s2.set((c) => c + 1); // 0 → 1
s2.set((c) => c + 1); // 1 → 2
assert.strictEqual(s2.render(), 2);
// 对象状态:必须整体替换,且手动展开旧值
const user = createState({ name: '小明', age: 18 });
user.set((u) => ({ ...u, age: 19 })); // 展开保留 name
assert.deepStrictEqual(user.render(), { name: '小明', age: 19 });
console.log('setCount(count+1) 两次:', s1.render(), '(批处理合并)');
console.log('setCount(c=>c+1) 两次:', s2.render(), '(函数式拿到最新值)');
真实组件:
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
function addThree() {
setCount((c) => c + 1); // 函数式:基于最新值
setCount((c) => c + 1);
setCount((c) => c + 1);
}
return (
<>
<p>{count}</p>
<button onClick={addThree}>+3</button>
<button onClick={() => setCount(0)}>归零</button>
</>
);
}
常见坑
- 连续 setState 基于旧值:循环或连续调用里必须用函数式
set(c => c + 1)。 - 直接改状态对象:
user.age = 19不触发渲染,必须 setUser 传新对象。 - 更新对象忘记展开:
setUser({ age: 19 })把 name 弄丢了,{ ...u, age: 19 }。 - 存可推导的数据:全名、过滤后的列表都该在渲染时算,多一份状态多一份不一致。
- 状态该提升不提升:两个兄弟组件各存一份再手动同步,bug 温床;提到共同父组件。
小结
useState 给组件跨渲染的记忆;setState 异步批处理,基于最新值用函数式更新;状态保持最小、只读替换、该提升就提升。下一章讲为什么必须「不可变更新」。