作为一名 Vue 开发者,你一定习惯了 Vue 3 响应式系统的便利性。当切换到 React 时,最大的不适应可能就是状态管理的方式。好消息是,Valtio 能让你在 React 中找回类似 Vue 的开发体验。
参考用例 : DEMO
为什么 Vue 开发者会喜欢 Valtio?
1. 熟悉的可变状态写法
在 Vue 中:
ts
// Vue 3 Composition API
import { reactive } from 'vue'
const state = reactive({
count: 0,
user: {
name: 'Alice'
}
})
// 直接修改
state.count++
state.user.name = 'Bob'
在 Pinia 中:
ts
// Pinia Store
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
user: {
name: 'Alice'
}
}),
actions: {
increment() {
this.count++
},
updateName(name: string) {
this.user.name = name
}
}
})
// 在组件中使用
const store = useCounterStore()
store.increment()
store.updateName('Bob')
在 Valtio 中:
ts
// React + Valtio
import { proxy } from 'valtio'
const state = proxy({
count: 0,
user: {
name: 'Alice'
}
})
// 同样直接修改!
state.count++
state.user.name = 'Bob'
看到了吗?写法几乎一模一样! 不需要像其他 React 状态库那样写 setState 或者返回新对象。
2. 自动的响应式追踪
Vue 的响应式:
ts
<template>
<div>{{ state.count }}</div>
<button @click="state.count++">增加</button>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
const state = reactive({ count: 0 })
</script>
Valtio 的响应式:
ts
import { proxy, useSnapshot } from 'valtio'
const state = proxy({ count: 0 })
function Counter() {
const snap = useSnapshot(state) // 类似 Vue 的自动依赖追踪
return (
<div>
<div>{snap.count}</div>
<button onClick={() => state.count++}>增加</button>
</div>
)
}
useSnapshot 就像 Vue 的响应式系统,只会在组件实际使用的状态改变时才重新渲染。
核心概念对比
| Vue 3 | Pinia | Valtio | 说明 |
|---|---|---|---|
reactive() |
state: () => ({}) |
proxy() |
创建响应式对象 |
ref() |
- | 不需要 | Valtio 的 proxy 可以直接修改基础类型 |
| 模板自动追踪 | storeToRefs() |
useSnapshot() |
追踪依赖并触发更新 |
computed() |
getters: {} |
getter 函数 | 计算属性 |
watch() |
$subscribe() |
subscribe() |
监听状态变化 |
| - | actions: {} |
类方法 | 定义状态修改方法 |
实战示例:从 Vue/Pinia 迁移到 Valtio
场景一:简单计数器
Vue 版本:
ts
<template>
<div>
<h1>{{ count }}</h1>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
const increment = () => count.value++
const decrement = () => count.value--
</script>
Pinia 版本:
ts
// stores/counter.ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
actions: {
increment() {
this.count++
},
decrement() {
this.count--
}
}
})
// 组件中使用
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
</script>
<template>
<div>
<h1>{{ store.count }}</h1>
<button @click="store.increment">+1</button>
<button @click="store.decrement">-1</button>
</div>
</template>
Valtio 版本 (推荐 Class 写法):
ts
import { proxy, useSnapshot } from 'valtio'
// 辅助函数
function bind<T extends object>(instance: T): T {
const obj = instance as any
const names = Object.getOwnPropertyNames(Object.getPrototypeOf(obj))
for (const name of names) {
const method = obj[name]
if (name === 'constructor' || typeof method !== 'function') continue
obj[name] = (...args: unknown[]) => method.apply(instance, args)
}
return instance
}
class ValtioStore {
constructor() {
return bind(proxy(this))
}
}
// 状态类 - 类型自动推导
class CounterStore extends ValtioStore {
count = 0 // 自动推导为 number
increment() {
this.count++
}
decrement() {
this.count--
}
}
export const counterStore = new CounterStore()
// 组件中使用
function Counter() {
const snap = useSnapshot(counterStore)
return (
<div>
<h1>{snap.count}</h1>
<button onClick={snap.increment}>+1</button>
<button onClick={snap.decrement}>-1</button>
</div>
)
}
场景二:嵌套对象和方法
Vue 版本:
ts
import { reactive } from 'vue'
const store = reactive({
user: {
name: '张三',
age: 25
},
updateName(newName: string) {
this.user.name = newName
},
incrementAge() {
this.user.age++
}
})
Pinia 版本:
ts
// stores/user.ts
import { defineStore } from 'pinia'
interface User {
name: string
age: number
}
export const useUserStore = defineStore('user', {
state: (): { user: User } => ({
user: {
name: '张三',
age: 25
}
}),
actions: {
updateName(newName: string) {
this.user.name = newName
},
incrementAge() {
this.user.age++
},
async fetchUser(id: number) {
const response = await fetch(`/api/user/${id}`)
const data = await response.json()
this.user = data
}
}
})
Valtio Class 版本:
ts
import { proxy } from 'valtio'
class ValtioStore {
constructor() {
return bind(proxy(this))
}
}
class UserStore extends ValtioStore {
user = {
name: '张三',
age: 25
} // 类型自动推导: { name: string; age: number }
updateName(newName: string) {
this.user.name = newName // 类型安全
}
incrementAge() {
this.user.age++
}
// 支持异步方法
async fetchUser(id: number) {
const response = await fetch(`/api/user/${id}`)
const data = await response.json()
this.user = data
}
}
export const userStore = new UserStore()
// 在组件中使用
function UserProfile() {
const snap = useSnapshot(userStore)
return (
<div>
<h2>{snap.user.name} - {snap.user.age}岁</h2>
<button onClick={() => userStore.updateName('李四')}>
改名
</button>
<button onClick={snap.incrementAge}>
增加年龄
</button>
</div>
)
}
场景三:推荐使用 Class 风格 ⭐
为什么推荐 Class?
- ✅ 类型自动推导 - 不需要手动定义类型
- ✅ 代码组织清晰 - 方法和状态在一起
- ✅ 更符合 OOP 思维 - 类似 Pinia 的 setup store 语法
Pinia Setup Store 写法:
ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
// state
const count = ref(0)
const user = ref({
name: '张三',
age: 25
})
// getters
const doubleCount = computed(() => count.value * 2)
// actions
function increment() {
count.value++
}
function decrement() {
count.value--
}
async function fetchUserAsync() {
await new Promise(resolve => setTimeout(resolve, 1000))
user.value.age++
}
function updateUser(name: string, age: number) {
user.value.name = name
user.value.age = age
}
return {
count,
user,
doubleCount,
increment,
decrement,
fetchUserAsync,
updateUser
}
})
Valtio Class 写法 (更简洁):
ts
import { proxy } from 'valtio'
// 辅助函数:绑定方法的 this
function bind<T extends object>(instance: T): T {
const obj = instance as any
const names = Object.getOwnPropertyNames(Object.getPrototypeOf(obj))
for (const name of names) {
const method = obj[name]
if (name === 'constructor' || typeof method !== 'function') continue
obj[name] = (...args: unknown[]) => method.apply(instance, args)
}
return instance
}
// 基类
class ValtioStore {
constructor() {
return bind(proxy(this))
}
}
// 你的状态类 - 无需定义类型!
class CounterStore extends ValtioStore {
count = 0 // TypeScript 自动推导为 number
user = {
name: '张三',
age: 25
} // 自动推导嵌套对象类型
// getter - 类似 Pinia 的 computed
get doubleCount() {
return this.count * 2
}
increment() {
this.count++ // this 指向正确,类型安全!
}
decrement() {
this.count--
}
async fetchUserAsync() {
// 支持异步方法
await new Promise(resolve => setTimeout(resolve, 1000))
this.user.age++
}
updateUser(name: string, age: number) {
this.user.name = name
this.user.age = age
}
}
// 导出单例
export const counterStore = new CounterStore()
// TypeScript 会自动推导出完整类型:
// counterStore.count: number
// counterStore.user: { name: string, age: number }
// counterStore.doubleCount: number (getter)
// counterStore.increment: () => void
在组件中使用:
ts
import { useSnapshot } from 'valtio'
import { counterStore } from './stores/counter'
function Counter() {
const snap = useSnapshot(counterStore)
return (
<div>
<h1>{snap.count}</h1>
<p>双倍: {snap.doubleCount}</p>
<p>{snap.user.name} - {snap.user.age}岁</p>
<button onClick={snap.increment}>+1</button>
<button onClick={snap.decrement}>-1</button>
<button onClick={snap.fetchUserAsync}>增加年龄</button>
</div>
)
}
对比 Pinia 和对象风格的优势:
ts
// ❌ Pinia:需要 ref/reactive 包装,return 暴露
export const useStore = defineStore('store', () => {
const count = ref(0)
const user = ref({ name: '张三', age: 25 })
function increment() { count.value++ }
function decrement() { count.value-- }
return { count, user, increment, decrement }
})
// ❌ Valtio 对象风格:需要手动定义类型
interface State {
count: number
user: { name: string; age: number }
increment: () => void
decrement: () => void
}
const state: State = proxy({
count: 0,
user: { name: '张三', age: 25 },
increment() { this.count++ },
decrement() { this.count-- }
})
// ✅ Valtio Class 风格:类型自动推导,代码更简洁
class CounterStore extends ValtioStore {
count = 0
user = { name: '张三', age: 25 }
increment() { this.count++ }
decrement() { this.count-- }
}
性能优化:和 Vue 一样智能
Vue 的性能特点:
- 自动追踪依赖,只更新用到的组件
- 深层对象变化也能精确响应
Valtio 的性能特点:
ts
// 父组件
function App() {
const snap = useSnapshot(state)
return (
<div>
<Counter /> {/* 只在 count 变化时重渲染 */}
<UserInfo /> {/* 只在 user 变化时重渲染 */}
</div>
)
}
// 子组件 1 - 只使用 count
function Counter() {
const snap = useSnapshot(state)
return <div>{snap.count}</div> // 只订阅 count
}
// 子组件 2 - 只使用 user
function UserInfo() {
const snap = useSnapshot(state)
return <div>{snap.user.name}</div> // 只订阅 user.name
}
关键点: 当 state.count 改变时,UserInfo 组件不会重新渲染,这和 Vue 的响应式系统行为一致!
监听状态变化 (类似 Vue 的 watch 和 Pinia 的 $subscribe)
Vue 的 watch:
ts
import { reactive, watch } from 'vue'
const state = reactive({ count: 0 })
watch(() => state.count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`)
})
Pinia 的 $subscribe:
ts
import { defineStore } from 'pinia'
const useStore = defineStore('counter', {
state: () => ({ count: 0 })
})
const store = useStore()
// 订阅整个 store 的变化
store.$subscribe((mutation, state) => {
console.log('状态变化了:', state.count)
})
Valtio 的 subscribe:
ts
import { proxy, subscribe } from 'valtio'
const state = proxy({ count: 0 })
const unsubscribe = subscribe(state, () => {
console.log('状态改变了:', state.count)
})
// 取消订阅
unsubscribe()
在 React 组件中使用:
ts
import { useEffect } from 'react'
function Logger() {
useEffect(() => {
const unsub = subscribe(counterStore, () => {
console.log('状态变化:', JSON.stringify(counterStore))
})
return () => unsub() // 组件卸载时清理
}, [])
return null
}
Valtio vs Zustand vs Pinia:该选哪个?
从 Vue/Pinia 过来的开发者,我建议优先选择 Valtio,原因如下:
Valtio 的优势 (最像 Vue/Pinia):
ts
// ✅ 直接修改,符合 Vue/Pinia 习惯
counterStore.count++
counterStore.user.name = 'Bob'
// 或者用方法
counterStore.increment()
Pinia 的写法:
ts
// Pinia 在 Vue 中
const store = useCounterStore()
store.count++ // 可以直接修改
store.increment() // 或调用 action
Zustand 的写法 (更像 Redux):
ts
// ❌ 需要调用 set 函数,不够直观
interface CounterState {
count: number
increment: () => void
}
const useStore = create<CounterState>(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 }))
}))
三者对比表格
| 特性 | Pinia | Valtio | Zustand |
|---|---|---|---|
| 可变状态 | ✅ 是 | ✅ 是 | ❌ 否 (不可变) |
| 学习曲线 | 低 (Vue 开发者) | 低 (类似 Pinia) | 中等 |
| TypeScript | ✅ 优秀 | ✅ 优秀 (Class 自动推导) | ✅ 优秀 |
| DevTools | ✅ Vue DevTools | ✅ 支持 | ✅ Redux DevTools |
| 中间件 | ✅ Plugins | ⚠️ 较少 | ✅ 丰富 |
| Class 支持 | ⚠️ Setup Store | ✅ 原生支持 | ❌ 不推荐 |
| 生态系统 | 大 (Vue) | 中等 | 大 (React) |
但是,如果你需要以下功能,可以考虑 Zustand:
- 中间件支持 (持久化、日志等)
- 选择器优化 (精确控制订阅)
- 更大的社区和生态
Valtio vs Pinia 的核心区别:
- Pinia 需要在 Vue 组件中使用
useStore()hook - Valtio 的 store 是全局单例,可以在任何地方使用
- Pinia 有 Vue DevTools 集成
- Valtio 更轻量,没有框架绑定
最佳实践总结
1. 组件分离 (类似 Vue 的组件化思想)
ts
// ❌ 不好:所有逻辑混在一起
function App() {
const snap = useSnapshot(counterStore)
return (
<div>
<div>{snap.count}</div>
<button onClick={snap.increment}>+</button>
</div>
)
}
// ✅ 好:控制和展示分离
function Counter() {
const snap = useSnapshot(counterStore)
return <h1>{snap.count}</h1>
}
function Controls() {
const snap = useSnapshot(counterStore)
return (
<div>
<button onClick={snap.increment}>+</button>
<button onClick={snap.decrement}>-</button>
</div>
)
}
2. 状态模块化 (推荐 Class 方式)
ts
// stores/counter.ts
import { proxy } from 'valtio'
class ValtioStore {
constructor() {
return bind(proxy(this))
}
}
export class CounterStore extends ValtioStore {
count = 0
increment() {
this.count++
}
decrement() {
this.count--
}
}
export const counterStore = new CounterStore()
ts
// stores/user.ts
interface User {
id: number
name: string
email: string
}
export class UserStore extends ValtioStore {
user: User | null = null
loading = false
async login(username: string) {
this.loading = true
try {
const user = await api.login(username)
this.user = user
} finally {
this.loading = false
}
}
logout() {
this.user = null
}
}
export const userStore = new UserStore()
3. 读用 snap,写用原始 store
ts
import { counterStore } from './stores/counter'
function Component() {
const snap = useSnapshot(counterStore) // ✅ 读取用 snap
return (
<div>
<div>{snap.count}</div> {/* 读 */}
<button onClick={counterStore.increment}> {/* 写 - 直接用 store */}
增加
</button>
{/* 或者用 snap 的方法也可以 */}
<button onClick={snap.decrement}>
减少
</button>
</div>
)
}
总结
从 Vue/Pinia 切换到 React,选择 Valtio 可以让你:
- ✅ 保持熟悉的可变状态写法 - 和 Vue reactive/Pinia 一样直观
- ✅ 享受类似 Vue 的响应式体验 - 自动依赖追踪
- ✅ Class 风格减少类型定义 - 类似 Pinia Setup Store,但更简洁
- ✅ 减少学习曲线,快速上手 - 从 Pinia 迁移几乎无缝
- ✅ 获得优秀的性能表现 - 精确的响应式更新
Vue/Pinia → Valtio 迁移速查表
| Pinia | Valtio | 说明 |
|---|---|---|
defineStore() |
class extends ValtioStore |
定义 store |
state: () => ({}) |
count = 0 |
定义状态 |
actions: {} |
类方法 | 定义操作 |
getters: {} |
get xxx() |
计算属性 |
useStore() |
useSnapshot(store) |
在组件中使用 |
store.$subscribe() |
subscribe(store) |
监听变化 |
storeToRefs(store) |
useSnapshot(store) |
响应式引用 |
如果你习惯了 Vue 的 reactive() 和 Pinia 的状态管理,那么 Valtio 的 proxy() 和 Class 写法会让你感到宾至如归。不需要重新学习完全不同的状态管理思维,就能在 React 中高效开发。
现在就开始你的 Valtio 之旅吧!🚀