一、useState 做的事就一件:给组件一个”记忆”
没有 useState 的组件是无记忆的——每次渲染都是全新计算,函数里的变量全部重置。有了 useState,组件能记住上一次的值,并且改了之后自动触发重渲染。
function Counter() {
const [count, setCount] = useState(0);
// ↑ 当前值 ↑ 改值的函数 ↑ 初始值
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
useState(0) 返回了什么?
它返回一个长度为 2 的数组:
const result = useState(0);
// result[0] → 当前值(第一次渲染是 0)
// result[1] → 修改值的函数
我们用解构赋值给这两个东西取了名字:
const [count, setCount] = useState(0);
// 等价于:
// const count = result[0]
// const setCount = result[1]
名字随你取,但有个约定俗成的规则:值叫 X,改值的函数叫 setX。
const [name, setName] = useState(""); // name / setName
const [isOpen, setIsOpen] = useState(false); // isOpen / setIsOpen
const [items, setItems] = useState([]); // items / setItems
setCount 是什么?
setCount 是 React 给你的一个函数。你调用它,传入一个新值,React 就会:
- 把
count更新为你传的值 - 自动触发组件重新渲染
setCount(5); // 你调用 → React 把 count 改成 5 → 组件重新渲染
setCount(10); // 你调用 → React 把 count 改成 10 → 组件重新渲染
你不需要知道 setCount 内部怎么实现——你只需要知道:调了它,值就变了,页面就自动更新了。
你可以把它理解成一个”遥控器”:你按一下(调用),React 就帮你改值 + 刷新页面。你不用自己动手改 DOM。
二、它和声明式的关系
useState 是 React 声明式链条的触发器:
setCount(1) ← 你做的事:调 setter 改状态
→ React 知道状态变了
→ 重新执行组件函数,拿到新的 Virtual DOM
→ Diff → 更新真实 DOM
你只管”数据变成什么”(setCount(1)),不用管”页面怎么跟着变”(React 自动处理)。这就是声明式——详见 声明式与 React 的实现机制。
useState不是声明式本身,而是声明式链条的起点。你拿useState一样可以写命令式代码(存 DOM ref 然后手动操作),只是没人会这么干。同一层的其他 API 如useReducer、useContext也是触发器——它们共享同一个”调了就触发重渲染”的机制。
三、更新机制
3.1 直接传值 vs 传函数
setCount 是 useState 返回的第二个值——一个由 React 提供的函数,用来修改状态并触发重渲染。它接受两种参数:
// 直接传值:你告诉它"新值是什么"
setCount(5); // count 变成 5,不管之前是多少
// 传函数:你告诉它"怎么从旧值算出新值"
setCount(prev => prev + 1); // 在旧值基础上 +1
// ↑ prev 是当前最新的 count 值
// ↑ 返回值是新值
类比:直接传值像跟快递员说”把包裹放在门口”——具体指令;传函数像说”包裹比昨天多放一个”——规则,不是具体数字。
什么时候必须用函数形式?当新值依赖旧值时:
// ❌ 连续调用时,两次 count 可能读到同一个旧值
setCount(count + 1); // 假设 count=0,两次都算出 1
setCount(count + 1); // 结果是 1,不是 2
// ✅ 函数形式保证每次拿到最新值
setCount(prev => prev + 1); // 0→1
setCount(prev => prev + 1); // 1→2,结果是 2
原因:React 会批量处理状态更新——多次 setState 可能被合并成一次渲染,直接传值时 count 是当前渲染闭包里的旧值,不是最新的。
简单判断:新值需要用到旧值 → 用函数形式;不需要 → 直接传值。
3.2 更新是”替换”不是”合并”
useState 的 setter 不会自动合并对象——它直接替换整个值:
const [user, setUser] = useState({ name: "yi", age: 25 });
// ❌ 直接改属性:React 检测不到变化(引用没变)
user.age = 26;
setUser(user);
// ❌ 只传部分字段:name 丢失了!
setUser({ age: 26 });
// ✅ 展开旧值再覆盖
setUser({ ...user, age: 26 });
// 等价于 { name: 'yi', age: 26 }
这和 class 组件的 this.setState({ age: 26 }) 不同——this.setState 会自动浅合并,useState 的 setter 不会。
3.3 引用类型与不可变性
对象和数组的更新必须创建新引用,React 才能检测到变化:
// 对象
setUser({ ...user, age: 26 }); // ✅ 新对象
setUser(Object.assign({}, user, { age: 26 })); // ✅ 新对象
// 数组
setItems([...items, newItem]); // ✅ 加一项(新数组)
setItems(items.filter(i => i.id !== id)); // ✅ 删一项(新数组)
setItems(items.map(i => (i.id === id ? { ...i, done: true } : i))); // ✅ 改一项
// ❌ 这些操作修改了原数组/原对象,React 检测不到变化
items.push(newItem);
setItems(items);
items.splice(0, 1);
setItems(items);
user.age = 26;
setUser(user);
原则:永远不要直接修改 state,总是创建新值。这就是 React 强调的”不可变性(Immutability)”。
四、常见用法模式
4.1 开关 / Toggle
const [isOpen, setIsOpen] = useState(false)
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
// 更安全的写法(避免闭包陷阱):
<button onClick={() => setIsOpen(prev => !prev)}>Toggle</button>
4.2 表单输入
const [name, setName] = useState('')
<input value={name} onChange={e => setName(e.target.value)} />
多个表单字段用对象管理:
const [form, setForm] = useState({ name: '', email: '' })
<input
value={form.name}
onChange={e => setForm({ ...form, name: e.target.value })}
/>
<input
value={form.email}
onChange={e => setForm({ ...form, email: e.target.value })}
/>
4.3 列表
const [items, setItems] = useState([]);
// 加
const addItem = item => setItems(prev => [...prev, item]);
// 删
const removeItem = id => setItems(prev => prev.filter(i => i.id !== id));
// 改
const toggleItem = id =>
setItems(prev => prev.map(i => (i.id === id ? { ...i, done: !i.done } : i)));
4.4 惰性初始化
当初始值需要昂贵计算时,传函数避免每次渲染都重新算:
// ❌ 每次渲染都执行 createInitialItems(),哪怕只用第一次
const [items, setItems] = useState(createInitialItems());
// ✅ 只在首次渲染时执行
const [items, setItems] = useState(() => createInitialItems());
useState 接收函数时,只在初始化时调用一次;接收值时,每次渲染都会先计算这个值(然后被丢弃)。
TanStack Query 的
const [qc] = useState(() => new QueryClient())就是这个模式——new QueryClient()只执行一次,后续渲染复用同一个实例。
五、易错点
5.1 闭包陷阱:读到了旧的 state
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
// 这里的 count 是渲染时的快照,不是"最新值"
console.log(count); // 可能是旧的
setCount(count + 1);
}
// 3 秒后拿到的 count 是点击那一刻的值,不是当前值
function handleDelay() {
setTimeout(() => {
setCount(count + 1); // ❌ 用的是闭包里的旧 count
}, 3000);
}
// ✅ 用函数形式拿到最新值
function handleDelayFix() {
setTimeout(() => {
setCount(prev => prev + 1); // ✅
}, 3000);
}
}
规律:只要”新值依赖旧值”且存在异步/延迟,就用函数形式
prev => ...。
5.2 在条件语句里调用 Hook
// ❌ Hook 调用顺序不能变,React 靠调用顺序匹配 state
if (condition) {
const [x, setX] = useState(0);
}
// ✅ 条件判断写在 Hook 内部
const [x, setX] = useState(0);
if (condition) {
// 用不用 x 是你的事,但 Hook 必须每次都调用
}
这是 React Hook 的铁律:Hook 必须在组件顶层调用,不能在条件、循环、嵌套函数中调用。原因:React 按调用顺序匹配 state——第一次渲染 useState 调了 3 次,第二次也必须调 3 次,顺序还一样,否则 state 就错位了。
5.3 setter 传相同的值不会触发重渲染
setCount(5); // 如果 count 已经是 5,React 会跳过这次渲染
React 用 Object.is() 比较新旧值,相同则不渲染。这对性能是好事——不需要你手动去防抖。
六、一句话
useState给组件添加记忆,你调 setter 改状态,React 自动重渲染。记住三件事:新值依赖旧值用函数形式、对象/数组必须创建新引用、Hook 必须在顶层调用。