本文围绕从 Vue3 到 React · 快速上手系列第 6 篇:状态管理 useState 与 useReducer实战解析展开,先梳理核心概念,再结合实践场景说明步骤、代码思路和容易忽略的细节,方便后续直接参考。
这是整个系列最核心的一篇。Vue 的响应式(ref / reactive)是它的灵魂,React 对应的就是 useState。但两者背后的机制截然不同——理解这个差异,你就真正"懂" React 了。

useState vs ref:第一眼 复制代码// Vue3 const count = ref(0) count.value++ // 直接改,视图自动更新 复制代码// React const [count, setCount] = useState(0) setCount(count + 1) // 调用 setter,请求重新渲染 useState(初始值) 返回一个数组:[当前值, 更新函数]。这里用了 JS 的数组解构,你可以随意命名,约定是 [x, setX]。
核心差异:
count 是个响应式容器,你改 .value,Vue 精准更新用到它的地方。count 就是本次渲染的一个普通数值快照;调用 setCount 不是"改变量",而是告诉 React"用新值重新渲染整个组件"。回顾第 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> } 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 的 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> ) } 选择建议:
useStateuseReducer| 需求 | 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(请求渲染),不能直接赋值。setX(prev => ...)。useState(() => ...) 处理昂贵初始值。useReducer 集中管理。下一篇讲副作用与生命周期 —— watch/watchEffect/onMounted 如何翻译为 useEffect——它一个 Hook 就统一了你熟悉的 watch、watchEffect、onMounted、onUnmounted。
实际使用时,建议结合项目规模、依赖环境和团队习惯做取舍;先保证流程清晰和结果可验证,再逐步优化细节。