从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 之旅吧!🚀

相关推荐
努力学习的少女2 小时前
对SparkRDD的认识
开发语言·前端·javascript
LYFlied2 小时前
Webpack 深度解析:从原理到工程实践
前端·面试·webpack·vite·编译原理·打包·工程化
苏打水com2 小时前
第十二篇:Day34-36 前端工程化进阶——从“单人开发”到“团队协作”(对标职场“大型项目协作”需求)
前端·javascript·css·vue.js·html
知了清语2 小时前
为天地图 JavaScript API v4.0 提供 TypeScript 类型支持 —— tianditu-v4-types 正式发布!
前端
程序员Sunday2 小时前
为什么 AI 明明写后端更爽,但却都网传 AI 取代前端,而不是 AI 取代后端?就离谱...
前端·后端
之恒君3 小时前
React 性能优化(方向)
前端·react.js
3秒一个大3 小时前
Vue 任务清单开发:数据驱动 vs 传统 DOM 操作
前端·javascript·vue.js
an86950013 小时前
vue自定义组件this.$emit(“refresh“);
前端·javascript·vue.js
Avicli3 小时前
Gemini3 生成的基于手势控制3D粒子圣诞树
前端·javascript·3d