基本用法

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 深入