基本用法
const [n, setN] = useState(0);
useState(initialValue) 返回 [当前值, 更新函数]。只有第一次渲染用 initialValue,之后忽略。
坑 1:陈旧闭包(stale closure)
const [n, setN] = useState(0);
useEffect(() => {
const t = setInterval(() => {
setN(n + 1); // ❌ n 永远是 0
}, 1000);
return () => clearInterval(t);
}, []); // [] 让 effect 只跑一次,但 n 也被冻在 0
修法:函数式更新
setN(prev => prev + 1); // ✅ 总是拿最新值
或把 n 加进依赖数组(但 effect 会反复重启,不推荐)。
坑 2:对象 / 数组的不可变更新
const [user, setUser] = useState({ name: 'A', age: 1 });
// ❌ 引用没变,React 看不出来
user.age = 2;
setUser(user);
// ✅
setUser({ ...user, age: 2 });
setUser(prev => ({ ...prev, age: prev.age + 1 }));
数组同理:[...arr, x]、arr.filter(...)、arr.map(...)——绝不用 push/pop/splice。
复杂嵌套用 immer:
import { produce } from 'immer';
setUser(produce(draft => { draft.profile.age = 2 }));
坑 3:连续多次 setState
function onClick() {
setN(n + 1);
setN(n + 1);
setN(n + 1);
}
n 从 0 → 期待 3,实际 1。因为这三次拿到的 n 都是同一次 render 的 0。
修法:
setN(prev => prev + 1);
setN(prev => prev + 1);
setN(prev => prev + 1); // → 3
React 18 起,事件回调里的多次 setState 会自动 batch,只触发一次 re-render。但每次拿到的 state 仍是 render 时的快照——还得用函数式更新。
坑 4:派生状态
// ❌ 不要把"能算出来的"放 state
const [items, setItems] = useState([]);
const [count, setCount] = useState(0); // 多余
useEffect(() => setCount(items.length), [items]);
// ✅ 直接算
const [items, setItems] = useState([]);
const count = items.length;
凡是"从其他 state 推导"出来的值——别放 state,render 时直接算。React 比你想的快。
坑 5:初始值是昂贵计算
const [tree] = useState(buildHugeTree()); // ❌ 每次 render 都跑(虽然结果被忽略)
修法:传函数
const [tree] = useState(() => buildHugeTree()); // ✅ 只在第一次渲染调用
state 应该放哪一层
放在所有需要它的组件的最近共同祖先——叫 "lifting state up"(状态上提)。
太高 → 不相关组件被 re-render;太低 → 兄弟组件拿不到。
实在跨层 → Context(第 09 篇)或状态管理库(第 15 篇)。
state 还是 ref
| state | ref | |
|---|---|---|
| 改变触发 re-render | ✅ | ❌ |
| 在 JSX 里渲染 | ✅ | ❌(不会响应) |
| 跨 render 存值 | ✅ | ✅ |
| 适合 | UI 状态 | DOM 引用、计时器 ID、上一个值 |
实用模式
toggle:
const [open, setOpen] = useState(false);
const toggle = () => setOpen(o => !o);
重置:通过 key 让整个子树重新挂载
<Form key={userId} /> // userId 变 → Form 内所有 state 重置
→ 下一篇 useEffect 深入