从 Vue3 到 React · 快速上手系列第 6 篇:状态管理 useState 与 useReducer

面向 Vue3 开发者的 React 快速上手系列

这是整个系列最核心的一篇。Vue 的响应式(ref / reactive)是它的灵魂,React 对应的就是 useState。但两者背后的机制截然不同------理解这个差异,你就真正"懂" React 了。


一、useState vs ref:第一眼

js 复制代码
// Vue3
const count = ref(0)
count.value++              // 直接改,视图自动更新
jsx 复制代码
// React
const [count, setCount] = useState(0)
setCount(count + 1)        // 调用 setter,请求重新渲染

useState(初始值) 返回一个数组:[当前值, 更新函数]。这里用了 JS 的数组解构,你可以随意命名,约定是 [x, setX]

核心差异:

  • Vue:count 是个响应式容器,你改 .value,Vue 精准更新用到它的地方。
  • React:count 就是本次渲染的一个普通数值快照 ;调用 setCount 不是"改变量",而是告诉 React"用新值重新渲染整个组件"

二、为什么必须用 setter,不能直接改?

回顾第 1 篇的核心:React 组件每次渲染都重新执行整个函数。普通变量每次都会被重置,所以 React 需要把状态存在自己内部(Fiber 里),每次渲染时通过 useState 把最新值"发"给你。

jsx 复制代码
function Counter() {
  const [count, setCount] = useState(0)

  // ❌ 直接改普通思维会失效
  const bad = () => { count = count + 1 }  // 视图不会更新

  // ✅ 必须通过 setter 请求渲染
  const good = () => setCount(count + 1)

  return <button onClick={good}>{count}</button>
}

三、state 是"快照",不是"实时值"

Vue 里改完 count.value 立刻就能读到新值。React 里,在同一次渲染中,state 永远是那次渲染时的值,不会中途变化

jsx 复制代码
function Demo() {
  const [count, setCount] = useState(0)

  const handleClick = () => {
    setCount(count + 1)
    console.log(count)  // 打印的还是旧值!新值下次渲染才生效
  }
  // ...
}

更经典的坑------连续调用多次:

jsx 复制代码
const handleClick = () => {
  setCount(count + 1)  // count 是 0,请求渲染为 1
  setCount(count + 1)  // count 还是 0,又请求渲染为 1
  setCount(count + 1)  // 结果只 +1,而不是 +3!
}

解决办法:用"函数式更新",参数是"上一个最新值":

jsx 复制代码
const handleClick = () => {
  setCount(prev => prev + 1)   // 0 -> 1
  setCount(prev => prev + 1)   // 1 -> 2
  setCount(prev => prev + 1)   // 2 -> 3  ✅
}

经验法则:只要新值依赖旧值,就用 setCount(prev => ...) 的函数式写法,能规避绝大多数"拿到旧值"的 bug。


四、对象与数组:不可变更新(Vue 开发者最不适应的点)

Vue 的 reactive 对象可以随便原地改:

js 复制代码
// Vue3
const user = reactive({ name: 'Tom', age: 20 })
user.age = 21               // 直接改,OK
const list = reactive([1, 2])
list.push(3)                // 直接 push,OK

React 的 state 必须创建新对象/新数组替换,不能原地修改

jsx 复制代码
const [user, setUser] = useState({ name: 'Tom', age: 20 })

// ❌ 原地改,React 察觉不到(引用没变)
user.age = 21; setUser(user)

// ✅ 用展开运算符创建新对象
setUser({ ...user, age: 21 })

数组同理,用返回新数组的方法(map/filter/concat/展开),别用 push/splice 这类原地修改:

jsx 复制代码
const [list, setList] = useState([1, 2, 3])

setList([...list, 4])                       // 新增
setList(list.filter(n => n !== 2))          // 删除
setList(list.map(n => n === 2 ? 20 : n))    // 修改

为什么? React 靠"引用是否变化"来判断要不要重渲染。原地修改引用没变,React 以为没变化。这条"不可变原则"是 React 的铁律。

💡 嫌手动展开麻烦?大型嵌套对象可以用 ImmeruseImmer),让你像 Vue 一样"直接改",它在底层帮你生成新对象。这是很多从 Vue 转来的人的救星。


五、多个状态:拆开还是合并?

Vue 常把相关数据放一个 reactive 对象。React 里两种都行,原则是:

jsx 复制代码
// 独立、无关的状态 → 拆开(推荐)
const [name, setName] = useState('')
const [age, setAge] = useState(0)
const [loading, setLoading] = useState(false)

// 强相关、总是一起更新的 → 合并成一个对象
const [form, setForm] = useState({ name: '', email: '', phone: '' })
setForm(prev => ({ ...prev, name: 'Alice' }))

六、惰性初始化:昂贵的初始值只算一次

如果初始值需要昂贵计算(比如读 localStorage),传一个函数useState,它只在首次渲染执行一次:

jsx 复制代码
// ❌ 每次渲染都执行 expensiveInit()
const [data, setData] = useState(expensiveInit())

// ✅ 只在首次渲染执行一次
const [data, setData] = useState(() => {
  const saved = localStorage.getItem('data')
  return saved ? JSON.parse(saved) : defaultData
})

这个坑正是因为"组件每次渲染都重跑整个函数"导致的------牢记第 1 篇的核心。


七、useReducer:复杂状态逻辑的利器

当一个状态有多种更新方式、逻辑复杂时(比如一个表单、一个购物车),useState 会散落一堆 setter。这时用 useReducer,把所有更新逻辑集中到一个 reducer 函数里。它类似于把 Pinia 的 actions 收拢在一起的思路:

jsx 复制代码
import { useReducer } from 'react'

// reducer:根据 action 返回新 state(注意仍然是不可变更新)
function reducer(state, action) {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 }
    case 'decrement': return { count: state.count - 1 }
    case 'set':       return { count: action.payload }
    default:          return state
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 })

  return (
    <div>
      <span>{state.count}</span>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'set', payload: 0 })}>清零</button>
    </div>
  )
}

选择建议:

  • 简单、独立的值 → useState
  • 多字段、多操作、更新逻辑复杂 → useReducer
  • 跨组件的全局状态 → Context 或 Zustand/Redux(第 11 篇讲)

八、对照速查

需求 Vue3 React
基本类型状态 ref(0) useState(0)
读取值 count.value count(直接用)
修改值 count.value++ setCount(c => c + 1)
对象状态 reactive({}) useState({}) + 展开更新
修改对象属性 obj.a = 1 setObj({...obj, a: 1})
数组增删 arr.push() setArr([...arr, x])
复杂状态逻辑 reactive + 方法 useReducer

小结

  • useState 返回 [值, setter],改状态只能调 setter(请求渲染),不能直接赋值。
  • state 是快照 :同一次渲染内不会变;新值依赖旧值时用 setX(prev => ...)
  • 不可变更新是铁律:对象/数组要创建新的替换,嫌麻烦可用 Immer。
  • 惰性初始化 useState(() => ...) 处理昂贵初始值。
  • 复杂状态逻辑用 useReducer 集中管理。

下一篇讲副作用与生命周期 ------ watch/watchEffect/onMounted 如何翻译为 useEffect ------它一个 Hook 就统一了你熟悉的 watchwatchEffectonMountedonUnmounted

相关推荐
hunterandroid1 小时前
Room 并发写入与事务一致性:从数据竞争到可靠落地
前端
天才熊猫君1 小时前
自动给所有 catch 块补上错误上报:从原理到落地
前端·javascript
朱涛的自习室1 小时前
Munk AI 桌面端「预告」
android·前端·人工智能
程序员包打听1 小时前
从 npx 到 moonx,moonbit 的野心与展望
前端·后端
laboratory agent开发1 小时前
工具调用失败后怎么办?重试分层与降级回路的三种路线
服务器·前端·网络
IMPYLH2 小时前
HTML 的 <dialog> 元素
前端·html
AI编程实验室2 小时前
用 npm + Three.js 做一颗西瓜:把夏天的清凉感放进浏览器
前端·后端·ai编程
渣波2 小时前
基于 Milvus 构建小说知识库 RAG,实现图书智能问答(天龙八部实战)
前端·后端
JavaGuide2 小时前
我最推荐的 4 个 AI 编程 Skills:grill-me、research、diagnosing-bugs、code-review
前端·后端·ai编程