useState
从状态是什么、为什么需要 useState,到更新机制、函数式更新与常见坑的系统讲解
先用一句话理解 useState
useState 是 React 函数组件里用来保存组件私有状态并在状态变化时触发重新渲染的 Hook。
你可以把它当成:“给函数组件加上可记忆的数据,并在数据变化时让 UI 自动刷新”。
为什么函数组件需要状态
函数组件本质上就是一个函数:输入 props,输出 UI。
问题在于:函数每次执行都会重新计算,如果你希望某个值能“跨渲染保留”,就需要一个机制让 React 帮你把值存起来,并在更新后重新调用组件函数——这就是 useState 做的事。
最基本用法
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>count: {count}</button>
}
这里发生了什么:
count:当前状态值setCount:更新状态的函数(不要直接改 count)useState(0):初始值为 0(仅在首次挂载时使用)
解构出来的两个值分别是什么
state 值:用于渲染
count 会参与渲染。当 React 渲染组件时,它会读取 count 的当前值来生成 UI。
setter:触发状态更新与重新渲染
setCount(next) 会告诉 React:“把这个状态更新为 next,并在合适时机重新渲染”。
注意:setCount 不是“立刻改变量”,而是发起一次更新请求。
初始值只会用一次
很多人会误以为每次渲染都会执行 useState(0) 并把 count 变回 0。
事实是:只有首次挂载时会用到初始值。后续渲染会忽略初始值参数,直接读到 React 保存的状态。
惰性初始化:初始值很贵时怎么写
如果初始值需要复杂计算,别写成这样:
const [value, setValue] = useState(expensiveCompute())
因为 expensiveCompute() 每次渲染都会执行(即使结果只用首次)。
应该写成函数形式:
const [value, setValue] = useState(() => expensiveCompute())
这叫惰性初始化:React 只在首次挂载时调用一次该函数。
更新状态的两种写法
直接给新值
setCount(count + 1)
适合:新值不依赖“上一次状态”的复杂链式更新场景。
函数式更新
setCount((prev) => prev + 1)
适合:新值依赖上一次状态,或你在同一事件里连续更新多次。
批量更新与“为什么 +2 没生效”
看这个例子:
setCount(count + 1)
setCount(count + 1)
很多人希望它加 2,但常见情况下只会加 1。原因是:两次都基于同一个旧的 count 计算。
正确写法:
setCount((prev) => prev + 1)
setCount((prev) => prev + 1)
这样 React 会把两次更新按顺序应用在前一次结果上。
setState 是异步的吗
更准确的说法:setState 是可被批处理的,不保证立刻反映到当前执行栈里的变量上。
比如:
console.log(count) // 0
setCount(1)
console.log(count) // 仍然是 0(这里还是旧渲染的值)
如果你需要在状态更新后做事,通常做法是用 useEffect 监听状态变化:
useEffect(() => {
// count 变化后执行
}, [count])
状态比较:相同值可能不会触发渲染
React 会用 Object.is 比较新旧 state。
- 如果
Object.is(prev, next)为 true,React 可能跳过渲染 - 对对象/数组尤其重要:你必须创建新引用
对象/数组状态:不要原地改
错误示例(原地修改):
const [user, setUser] = useState({ name: 'A', age: 18 })
user.age += 1
setUser(user)
正确示例(创建新对象):
setUser((prev) => ({ ...prev, age: prev.age + 1 }))
数组同理:
setList((prev) => [...prev, newItem])
setList((prev) => prev.filter((x) => x.id !== id))
setList((prev) => prev.map((x) => (x.id === id ? { ...x, done: true } : x)))
setState 传入函数 vs state 本身是函数
如果你的 state 就是一个函数,你会遇到一个小坑:
// 你想把 fn 作为 state 存起来
setValue(fn)
React 会把 fn 当成“函数式更新器”来调用。
解决:用一层包裹:
setValue(() => fn)
同理初始化也一样:
const [fn, setFn] = useState(() => someFunction)
多个 useState 的组织方式
多个独立状态
const [name, setName] = useState('')
const [age, setAge] = useState(0)
优点:更新局部更简单,避免不必要的对象合并。
合并为一个对象
const [form, setForm] = useState({ name: '', age: 0 })
优点:表单类状态集中管理。
但要注意:更新对象要手动合并(不会自动 merge):
setForm((prev) => ({ ...prev, name: 'B' }))
何时考虑 useReducer 而不是 useState
当你发现出现这些信号时,可以考虑 useReducer:
- 状态字段很多,更新逻辑分散到各处
- 同一交互会触发多个字段协同更新
- 更新规则复杂,想更可预测、更可测试
useState 适合“简单状态”,useReducer 适合“复杂状态机”。
常见误区清单
- 把 state 当普通变量直接修改(尤其对象/数组)
- 连续更新依赖旧值却不用函数式更新
- 以为 setState 立刻改变当前变量
- 初始值写成昂贵函数调用导致每次渲染都计算
- 把函数当 state 存时忘了包一层
记忆模型
你可以用下面这个心智模型:
- 每次渲染:组件函数执行一次,拿到“当前那一帧”的 state
- 调用 setState:提交更新请求
- React 处理更新:决定何时重新渲染
- 重新渲染:函数再执行一次,读到新的 state,UI 更新
继续阅读
留言讨论
Discussion
欢迎交流与反馈