Valtio 全局状态管理工具
Valtio 通过代理(Proxy)技术实现了状态的追踪,使得状态变更自动触发依赖的更新,无需手动管理订阅者或优化更新逻辑
valtio 的特性:
1、自动追踪: Valtio 会自动追踪状态对象中的变更,无需手动调用 setter 函数 (如 setState),这使得状态管理更加自然和直接。
2、响应式: Valtio 状态是响应式的,意味着当状态改变时,依赖该状态的组件会自动重新渲染类似于使用 React 的 useEffect 配合 useState。
1、基础使用
1、创建状态对象:
使用 proxy
函数创建一个可追踪的状态对象:
import { proxy } from 'valtio';
const state = proxy({ count: 0 });
2、更新状态:
直接修改状态对象的属性即可触发更新:
注意:这里只能修改代理数据,不能直接修改快照(快照是只读的)
state.count++;
3、表单管理的双向绑定:
Valtio 的响应式特性非常适合实现表单的双向数据绑定,无需额外的 onChange 事件处理。
function Form(){
const state = proxy({ name:'', email:'' });
return (
<>
<input value = {state.name} onChange = {e => (state.name = e.target.value)} />
<input value = {state.email} onChange = {e => (state.email = e.target.value)} />
</>
);
}
上面这种直接使用proxy
创建局部状态,适用于组件内部的状态管理,不涉及跨组件共享。
4、如何提交状态数据给后端?
直接拿到快照
const handleSubmit = () => {
console.log(snapshot(proxyState));
}
5、实现点击按钮,将输入框的内容添加到页面中
const todoState = proxy({
list : []
})
const TodoList = () => {
const todoConsumer = useProxy(todoState)
const handleAdd = () => {
const ele = {
id : Date.now(),
text : '???',
done : false
}
todoConsumer.list.push(ele)
}
const handleDone = (id:string|number) => {
const ele = todoConsumer.list.find(e => e.id === id)
ele.done = true
}
<input value = {todoConsumer.val} onChange = {e => (todoConsumer.val = e.target.value)} />
{todoConsumer.list?.map(e => {
return (
<div key={e.id}>
{e.done ? <del>{e.text}</del> : <li>{e.text}</li>}
<button onclick={() => handleDone()}>done!</button>
</div>
)
})}
}
6、useProxy 的使用
通过createStore
创建了一个全局可共享的状态对象,适合多个组件共享同一份状态的情况
import { createStore, useProxy } from 'valtio';
import React from 'react';
// 创建状态 store
const state = createStore({ count: 0 });
function Counter() {
// 使用 useProxy Hook 来获取状态的代理对象
const proxy = useProxy(state);
function increment() {
// 直接修改代理对象的属性触发更新
proxy.count++;
}
return (
<div>
<p>Count: {proxy.count}</p >
<button onClick={increment}>Increment</button>
</div>
);
}
export default Counter;
2、结合 Axios 做数据请求和数据保存
import { proxy } from 'valtio';
import axios from 'axios';
// 状态存储
const state = proxy({ items: [] });
async function fetchAndStoreData() {
const data = await fetchData();
// 使用 splice (删除从索引0开始的state.items.length个元素)替换数组内容,确保响应性
state.items.splice(0, state.items.length, ...newData);
//如果只是想追加数据,可以使用.push
//state.items.push(...newData);
//state.items = data;
// 注意这里直接赋值可能会导致响应性丢失,可考虑使用数组的特定方法(如`.splice`, `.push`)来保持响应性
}
// 在组件中使用
function ItemsList() {
// 使用 useProxy 来获取响应式数据
const items = useProxy(state).items;
useEffect(() => {
fetchAndStoreData();
}, []); // 确保只在组件挂载时执行一次
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
要在多个组件间共享状态,只需在每个组件中使用 useProxy 来访问同一个状态对象即可。如果状态对象是在一个单独的模块中定义的(如上述的 state),确保所有使用该状态的组件都能访问到这个模块。
3、在 react 中使用
不需要注册,不需要引入一个 Provider 或者 Root 什么根组件来包裹 App 组件,直接新建一个 store 文件夹,然后创建 modules 和 index.ts,如下所示:
src/store/index.ts
src/store/modules/counter.ts
- store:整个应用的状态管理
- modules:存放各个 store,proxy 是自由的,没有约束单一状态源
- index.ts:导出 modules 中的各个 store
// index.ts
export * from './modules/counter'
// counter.ts
import { proxy } from 'valtio'
export const counterStore = proxy({
// state
count: 0,
// action
increase: () => {
counterStore.count++
},
// action
decrease: () => {
counterStore.count--
}
})
上面的 count
就相当于一个 state,increase
和 decrease
就是 actions,负责对状态进行修改。使用起来也相当简单:
// components/CompA.index.tsx
import { counterStore } from '~/store'
import { useSnapshot } from 'valtio'
export function CompA() {
const { count, increase } = useSnapshot(counterStore)
return (
<div>
<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
4、actions 的更多写法
上面的示例中,我使用了合并 state 和 acions 的写法,Valtio 还支持更多写法,任君挑选
1、单独分开写法
export const state = proxy({
count: 0,
})
export const increase = () => {
++state.count
}
export const decrease = () => {
--state.count
}
2、方法合并式写法
export const state = proxy({
count: 0,
})
export const actions = {
increase: () => {
++state.count
},
decrease: () => {
--state.count
},
}
5、计算属性
在 Valtio 中,没有直接提供这类 api,但是我们可以使用 subscribeKey
和 subscribe
来订阅某个状态的更新,从而即时的计算属性。
import { proxy } from 'valtio'
import { subscribeKey } from 'valtio/utils'
const initialState = {
count: 0
}
export const counterStore = proxy({
count: initialState.count,
// computed,需要手动订阅更新
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
})
6、状态组合
需求:在一个 store 中来使用另一个 store。
在 Valtio 中,状态组合也非常简单,直接引入使用即可,如果是在不同文件中的 store,则需要进行订阅更新。
我们新建一个 hello.ts:
src/modules/counter.ts
src/modules/hello.ts
// 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)
}
})
// 监听 counterStore 的更新
watch((get) => {
get(counterStore)
helloStore.greets = Array.from({ length: counterStore.count }, () => initGreet)
})
7、数据持久化
基本思路是订阅某个你需要持久化的 state,然后检测到更新到时候,即时的存一下 Storage 即可,每次获取的时候就从 Storage 中获取。
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
})
部分摘录自:https://juejin.cn/post/7225934630506643513?searchId=2024050908335108E8BA844ACF7B83BDC7