从Vue 到 React:Valtio 让状态管理更熟悉

作为一名 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 之旅吧!🚀

相关推荐
恋猫de小郭5 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅11 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606112 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了12 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅12 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅12 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅13 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment13 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅13 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊13 小时前
jwt介绍
前端