关于我 壹文 项目 三五好友
Valtio的初步认识 2024-05-09

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,increasedecrease 就是 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,但是我们可以使用 subscribeKeysubscribe 来订阅某个状态的更新,从而即时的计算属性。

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

Not-By-AI