React useState
React 中最基础的状态 Hook,用于让函数组件记住会影响渲染的值。
[!info] related notes
- 所属 MOC: React MOC
- 上位主题: react-hooks, react
- 易混淆概念: React 中的 props、state 和 ref, ECMAScript原始值与引用值
- 对照理解: React 状态模型 vs Vue3 响应式模型
- 面试专题: react-hooks-interview-deep-dive
React useState
useState 是 React 最基础的状态 Hook。它让函数组件记住会影响渲染的值。
一句话定义
如果一个值会变化,而且变化后应该反映到界面上,就通常可以考虑 useState。
最小例子
const [count, setCount] = useState(0)
这里的含义是:
count是当前状态值setCount是请求 React 更新状态的函数0是初始值
它和普通变量的差别
- 普通变量变化不会触发重渲染
useState管理的值变化后,React 会重新执行组件,计算新的 UI
再进一步理解:state 更像渲染快照
在函数组件里,你拿到的 state 更像“当前这次渲染对应的一份快照”。
所以:
setXxx不是立刻改掉当前变量- 更像是提交一次更新请求
- 下一次渲染时,你才会读到新的 state
如果要和 Vue 3 的响应式模型正面对照,继续看:React 状态模型 vs Vue3 响应式模型
什么时候适合用它
- 输入框内容
- 弹窗开关
- 当前选中项
- 加载状态、错误状态
核心概念:不可变更新
这是 useState 最重要的习惯,必须掌握。
为什么不能直接修改
const [user, setUser] = useState({ name: 'Alice', age: 18 })
// 错误写法
user.age = 19
setUser(user)
React 更擅长根据「引用是否变化」判断状态有没有更新。你直接改了原来的 user 对象,然后又把同一个对象引用传回去,React 感受不到变化。
推荐写法:创建新对象
setUser({
...user,
age: 19,
})
这里创建了一个新对象,...user 先复制原来的字段,age: 19 再覆盖掉旧值。
数组也是同理
const [list, setList] = useState([1, 2, 3])
// 错误:push 直接修改原数组
list.push(4)
setList(list)
// 推荐:创建新数组
setList([...list, 4])
// 或
setList(list.concat(4))
常见可变方法要小心
这些方法会直接修改原数组:
push/popshift/unshiftsplicesort/reverse
常用更新模式
// 更新对象某个字段
setForm({
...form,
name: 'Tom',
})
// 数组新增项
setList([...list, newItem])
// 数组删除项
setList(list.filter(item => item.id !== id))
// 数组替换某一项
setList(list.map(item =>
item.id === id ? { ...item, done: true } : item
))
函数式更新
为什么需要函数式更新
// 不推荐:连续更新可能有闭包问题
setCount(count + 1)
setCount(count + 1) // 两次都基于同一个旧 count
// 推荐:基于上一个状态计算
setCount(prev => prev + 1)
setCount(prev => prev + 1) // 两次都会加 1
函数式更新拿到的是最新状态,在连续更新、定时器回调、异步回调中更稳定。
对象也适用
setUser(prev => ({
...prev,
age: prev.age + 1,
}))
惰性初始化
如果初始值计算很昂贵,用函数传入,只在初始渲染时计算一次:
const [value, setValue] = useState(() => expensiveInit());
常见误区
1. 以为 setState 后立刻能读到新值
setCount(count + 1)
console.log(count) // 还是旧值!
setCount 更像是「告诉 React 请用这个新状态在下一次渲染时更新界面」。
2. 把可推导出的值也塞进 state
// 不推荐:fullName 可以由 firstName + lastName 推导出来
const [fullName, setFullName] = useState('')
useEffect(() => {
setFullName(firstName + ' ' + lastName)
}, [firstName, lastName])
// 推荐:渲染时直接计算
const fullName = firstName + ' ' + lastName
3. 直接修改对象或数组再传回去
这就是上面「不可变更新」要解决的核心问题。
最短记忆方式
useState 管的是「组件的记忆」,不是普通局部变量。
面试要点
来自 react-state-update-sync-vs-async-interview-question 的面试视角整理。
一句话回答
React 的状态更新本质上是一次更新请求和调度过程,不能简单粗暴地概括成同步或异步;更准确地说,调用更新函数后,当前执行上下文里通常不会立刻拿到新的渲染结果。
最稳的回答主线
- 调用
setState或setXxx不等于立刻重渲染完成 - React 会统一安排更新和渲染
- 所以在同一个事件处理函数里,马上读取旧变量,往往还是旧值
一个更完整的面试表达
可以这样答:
我不会简单回答 React 更新是同步还是异步。更准确的说法是,调用
setState或setXxx是在发起一次更新请求,React 会按自己的调度和批处理机制去安排后续渲染。
所以在当前这次执行上下文里,通常不能假设调用更新函数后,马上就能读到新的渲染结果。
一个最典型的例子
function Counter() {
const [count, setCount] = useState(0)
function handleClick() {
setCount(count + 1)
console.log(count) // 这里通常还是旧值
}
return <button onClick={handleClick}>{count}</button>
}
这里最重要的不是背“同步还是异步”,而是理解:
- 更新请求已经发出
- 但新的渲染结果还没在当前同步代码里可见
为什么函数式更新经常一起被问
因为它能避免闭包旧值和连续更新问题:
setCount(c => c + 1)
setCount(c => c + 1)
这种写法比连续写 setCount(count + 1) 更稳。
为什么面试官爱问这题
因为他想确认你是否理解“状态更新”和“界面已经完成新一轮渲染”不是同一时刻。
一句更工程化的表达
与其说 React 更新是异步,不如说它是可批处理、可调度的,重点是不要假设调用更新函数后界面状态立刻同步完成。
最短记忆方式
React 更新看调度,不要期待调用后立刻拿到新渲染结果。