React useState

React 中最基础的状态 Hook,用于让函数组件记住会影响渲染的值。

#tech / dev / frame #resource / react #type / concept #status / evergreen

[!info] related notes

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 / pop
  • shift / unshift
  • splice
  • sort / 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 的状态更新本质上是一次更新请求和调度过程,不能简单粗暴地概括成同步或异步;更准确地说,调用更新函数后,当前执行上下文里通常不会立刻拿到新的渲染结果。

最稳的回答主线

  • 调用 setStatesetXxx 不等于立刻重渲染完成
  • React 会统一安排更新和渲染
  • 所以在同一个事件处理函数里,马上读取旧变量,往往还是旧值

一个更完整的面试表达

可以这样答:

我不会简单回答 React 更新是同步还是异步。更准确的说法是,调用 setStatesetXxx 是在发起一次更新请求,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 更新看调度,不要期待调用后立刻拿到新渲染结果。

创建于 2026/3/19 更新于 2026/5/27