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

作者:袖梨 2026-07-30

本文围绕从 Vue3 到 React · 快速上手系列第 6 篇:状态管理 useState 与 useReducer实战解析展开,先梳理核心概念,再结合实践场景说明步骤、代码思路和容易忽略的细节,方便后续直接参考。

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

img_6a6aebd9261ac30.webp


一、useState vs ref:第一眼

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

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

核心差异:

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

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

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

 复制代码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 永远是那次渲染时的值,不会中途变化

 复制代码function Demo() { const [count, setCount] = useState(0) const handleClick = () => { setCount(count + 1) console.log(count) // 打印的还是旧值!新值下次渲染才生效 } // ... } 

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

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

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

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

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

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

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

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

 复制代码const [user, setUser] = useState({ name: 'Tom', age: 20 })// 原地改,React 察觉不到(引用没变) user.age = 21; setUser(user)// 用展开运算符创建新对象 setUser({ ...user, age: 21 }) 

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

 复制代码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 的铁律。


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

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

 复制代码// 独立、无关的状态 → 拆开(推荐) 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,它只在首次渲染执行一次:

 复制代码// 每次渲染都执行 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 收拢在一起的思路:

 复制代码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> ) } 

选择建议:

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

八、对照速查

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

小结

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

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

实际使用时,建议结合项目规模、依赖环境和团队习惯做取舍;先保证流程清晰和结果可验证,再逐步优化细节。

相关文章

精彩推荐