面向 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 的铁律。
💡 嫌手动展开麻烦?大型嵌套对象可以用 Immer(
useImmer),让你像 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 就统一了你熟悉的 watch、watchEffect、onMounted、onUnmounted。