首页/文章/javascript

valtio使用方式

2023-12-21
20647 分钟
...

前言

跳过前言:基本使用,您可以选择直接从第二节开始阅读。

提到 React 状态管理,我最初是接触的 Context,就是用 useContext 和 useReducer 去做状态管理,写多了发现还是挺麻烦的,还会出现 “Provider 嵌套地狱” 的问题,对于不同的 state 也不好组合计算。后面了解到 Redux,固有的模式使得用户需要编写很多重复和复杂的代码,甚至开发者也说了 “Try MobX”。对于 MobX,和前者的的函数式编程不同,它采用的是面向对象式的对状态进行管理,我本身并不是很习惯面向对象,这些状态管理库的心智负担,都太大了些。

现在我要推荐今天的主角——Valtio,这是我见过的使我的心智负担最低、需要编写的代码量最少的状态管理库,我本身也写 Vue3,我使用 Valtio 的感受就相当于,用了很久的 VueX,然后遇到了 Pinia!

Valtio 的优点:

  1. 概念简单,就是一个 proxy
  2. 文档友好,各种应用场景都有举例
  3. 使用方式和 API 简单,易于上手和使用,几乎没有什么心智负担...
  4. devtools api,完美支持 Debug
  5. 当然,完全支持 TypeScript

使用体验下来,简直就是 React 版本的 Pinia 😍

下面,我将类比 Pinia,来讲讲如何使用 Valtio 和管理应用状态。

基本使用

首先使用 Vite 创建一个 React + TS 项目,这个不用讲了。不需要注册,不需要引入一个 Provider 或者 Root 什么根组件来包裹 App 组件,直接新建一个 store 文件夹,然后创建 modules 和 index.ts,如下所示:

  • store:整个应用的状态管理
    • modules:存放各个 store,proxy 是自由的,没有约束单一状态源
    • index.ts:导出 modules 中的各个store

export * from './modules/counter'

import { proxy } from 'valtio'

export const counterStore = proxy({
  
  count: 0,
  
  increase: () => {
    counterStore.count++
  },
  
  decrease: () => {
    counterStore.count--
  }
})

上面的 count 就相当于一个 state,increasedecrease 就是 actions,负责对状态进行修改。使用起来也相当简单:


import { counterStore } from '~/store'
import { useSnapshot } from 'valtio'

export function CompA() {
  const { count, increase } = useSnapshot(counterStore)
  return (
    <div>
      CompA
      <div>count: {count}</div>
      <button onClick={increase}>+</button>
    </div>
  )
}

这里使用了 useSnapshot api,是为了保持 count state 的响应式,这样 Valtio 就会自动追踪更新,然后触发组件的 re-render,当然,这是可选的。

如果你要避免组件的 re-render:

const { count } = counterStore

如果你仅仅需要 actions 来更新状态:

const { increase } = counterStore

actions 的更多写法

上面的示例中,我使用了合并 state 和 acions 的写法,Valtio 还支持更多写法,任君挑选。

  1. 单独分开写法```ts export const state = proxy({ count: 0, })

    export const increase = () => { ++state.count }

    export const decrease = () => { --state.count }

  2. 方法合并式写法```ts export const state = proxy({ count: 0, })

    export const actions = { increase: () => { ++state.count }, decrease: () => { --state.count }, }

  3. this 写法```ts export const state = proxy({ count: 0, increase() { ++this.count }, decrease() { --this.count }, })

  4. class 写法```ts class State { count = 0 increase() { ++this.count } decrease() { --this.count } }

    export const state = proxy(new State())

计算属性

在 Pinia 中,我们可以直接使用 computed 来基于一个 state 进行计算,结果依然是响应式的。在 Valtio 中,没有直接提供这类 api,但是我们可以使用 subscribeKeysubscribe 来订阅某个状态的更新,从而即时的计算属性。

import { proxy } from 'valtio'
import { subscribeKey } from 'valtio/utils'

const initialState = {
  count: 0
}

export const counterStore = proxy({
  count: initialState.count,
  
  double: initialState.count * 2,
  update: (value: number) => {
    counterStore.count = value
  }
})


subscribeKey(counterStore, 'count', () => {
  counterStore.double = counterStore.count * 2
})

其中,subscribeKey 用于 primitive state(原始值类型),subscribe 用于引用类型(这里一般指 plain object)。

当然,你也可以不指定订阅某个状态,而直接使用 watch api,Valtio 会自动追踪依赖值。

watch((get) => {
  get(counterStore)
  counterStore.double = counterStore.count * 2
})

状态组合

需求:在一个 store 中来使用另一个 store。

在 Valtio 中,状态组合也非常简单,直接引入使用即可,如果是在不同文件中的 store,则需要进行订阅更新。

我们新建一个 hello.ts:



import { counterStore } from './counter'
import { watch } from 'valtio/utils'
import { proxy } from 'valtio'

const initGreet = 'hello counter'

export const helloStore = proxy({
  greets: Array.from({ length: counterStore.count }, () => initGreet),
  add: (value: string) => {
    helloStore.greets.push(value)
  }
})


watch((get) => {
  get(counterStore)
  helloStore.greets = Array.from({ length: counterStore.count }, () => initGreet)
})

功能:上面代码中,每次 count 更新的时候,greets 都会更新,计算关系为 greets 数组长度等于 count,每个元素都是 'hello counter'

greets.length === count

数据持久化

得益于 Valtio 的自由和简洁,你完全可以使用现有的 api 做到这点,基本思路是订阅某个你需要持久化的 state,然后检测到更新到时候,即时的存一下 Storage 即可,每次获取的时候就从 Storage 中获取。(仅需要两行代码)

Storage 可以是 localStorage 和 sessionStorage

示例代码:

import { proxy } from 'valtio'
import { subscribeKey } from 'valtio/utils'

const initialState = {
  count: 0
}

export const counterStore = proxy({
   
  count: Number(localStorage.getItem('count') ?? initialState.count),
  double: initialState.count * 2,
  update: (value: number) => {
    counterStore.count = value
  }
})

subscribeKey(counterStore, 'count', () => {
  
  localStorage.setItem('count', counterStore.count.toString())
})


watch((get) => {
  get(counterStore)
  counterStore.double = counterStore.count * 2
})

历史记录

历史记录?没错,Valtio 还支持状态的回退和前进,因为 Valtio 保存了状态的每一个 snapshot(状态快照),我们可以使用 proxyWithHistory 来创建一个可保存历史状态记录的 proxy,该方法创建的 proxy 暴露了 undoredo 方法能让我们对状态进行回退和复现,相当于 ctrl z 和 ctrl y。

import { proxyWithHistory } from 'valtio/utils'

export const counerStore = proxyWithHistory({
  count: 0,
  increase: () => {
    counerStore.value.count++
  },
  decrease: () => {
    counerStore.value.count--
  }
})

使用的时候主要这里需要使用 .value 来获取 state 和 actions。


import { useSnapshot } from 'valtio'
import { counerStore } from '~/store/modules/counter2'

export function CompC() {
  const counter = useSnapshot(counerStore)
  const { count, increase } = counter.value
  return (
    <>
     <div>{count}</div>
     <button onClick={increase}>+</button>
     <br />
     <button onClick={counter.undo}>undo</button>
     <button onClick={counter.redo}>redo</button>
    </>
  )
}

devtools debug

我们知道,pinia 支持使用 vue devtools 进行状态的追踪和 debug,那么我们的 Valtio 也支持 redux devtools。

下载好 redux devtools 插件后,在代码之加入下面这一行代码,即可轻松开启 debug:

devtools(counterStore, { name: 'state name', enabled: true })

一切都是那么的自然,几乎没有什么学习的负担,文档友好,api 简单,仅 3.1 kb 的库,赶快使用它来提升你的开发效率吧!(●'◡'●)

更多请参考官方文档:Valtio, makes proxy-state simple for React and Vanilla

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
javascript2026-02-24
navigator.sendBeacon全指南
在前端开发中,埋点系统是必不可少的一环。我们经常需要在用户 关闭页面 、 刷新 或 跳转路由 时,向服务器发送最后一条统计数据(比如用户停留时长、页面跳出率)。 但这看似简单的需求,在实现时却危机四伏:请求发不出去?页面跳转卡顿?今天我们就来聊聊这个问题的终极解决方案 —— 。 一、 痛点与传统方案的挣扎 场景还原 当用户点击关闭按钮时,浏览器会触发生命周期事件( 或 )。如果我们直接使用普通的异步 AJAX ( 或 ) 发送请求,浏览...
学习
javascript2025-11-02
理解浏览器事件系统,从用户点击到事件对象的完整旅程
深入理解浏览器事件系统:从用户点击到事件对象的完整旅程 “当我点击页面按钮时,背后发生了什么?为什么回调函数能收到一个包含丰富信息的event对象?今天,让我们一起揭开浏览器事件系统的神秘面纱。” 一个令人困惑的现象 作为前端开发者,我们每天都在写这样的代码: 这段代码如此熟悉,以至于我们很少停下来思考:​ ​这个 对象到底从哪里来?它为什么能知道点击的精确坐标?为什么能识别是哪个元素被点击了?​ ​ 更神奇的是,当我们手动创建事件时:...
学习
javascript2025-10-01
实现大文件上传全流程详解
在日常开发中,大文件上传是个绕不开的坎——动辄几百 MB 甚至 GB 级的文件,直接上传不仅容易超时,还会让用户体验大打折扣。最近我用 Vue+Express 实现了一套完整的大文件上传方案,支持分片上传、断点续传、秒传和手动中。 一、先看效果:我们要实现什么? 先上核心功能清单,确保大家明确目标,知道我们要解决哪些实际问题: 大文件分片上传 :将文件切成固定大小的小片段分批上传,避免单次请求超时 秒传 :服务器已存在完整文件时,直接返...
学习
javascript2025-09-18
JavaScript 的多线程能力:Worker
如果你写过一些计算量稍大的 JavaScript 代码,比如图像处理、大量数据排序或者复杂的算法,你几乎肯定遇到过浏览器“卡死”的现象。点击页面没反应,动画也停了,就像整个世界都静止了。 这就是主线程被阻塞的典型后果。因为主线程既要负责执行 JavaScript,又要负责渲染页面、响应用户操作,一旦它被繁重的计算任务占满,就无暇顾及其他,用户体验便直线下降。 这个问题的根源,正是“主线程是单线程的”。那么,如何解决呢? 答案很简单:把这...
学习面试
javascript2025-09-15
一张 8K 海报差点把首屏拖垮
你给后台管理系统加了一个「企业风采」模块,运营同学一口气上传了 200 张 8K 宣传海报。首屏直接飙到 8.3 s,LCP 红得发紫。 老板一句「能不能像朋友圈那样滑到哪看到哪?」——于是你把懒加载重新翻出来折腾了一轮。 解决方案:三条技术路线,你全踩了一遍 1\. 最偷懒:原生 一行代码就能跑,浏览器帮你搞定。 🔍 关键决策点 2020 年后现代浏览器全覆盖,IE 全军覆没。 必须写死 ,否则 CLS 会抖成 PPT。 适用场景...
学习
javascript2025-09-10
🚀 Web Worker让你的应用丝滑
🌟 引言 在日常的前端开发中,你是否遇到过这样的困扰: 大数据处理时页面卡死 :处理几万条数据时,页面直接卡成PPT,用户点击毫无反应 复杂计算阻塞UI :图片处理、数据分析等计算密集型任务让整个应用假死 文件上传/下载卡顿 :大文件操作时,其他功能完全无法使用 实时数据处理性能差 :WebSocket接收大量数据时,页面渲染严重滞后 今天分享6个Web Worker的核心技巧,让你的应用告别卡顿,用户体验丝滑如德芙! 💡 核心技巧...
学习

评论

请登录后发表评论

去登录
加载评论中...

目录