likes
comments
collection
share

【Valito入门】一个很好用的React响应式状态库

作者站长头像
站长
· 阅读数 42

本篇文章同时收录在公众号《泡芙学前端》,持续更新内容中,欢迎关注~

1.Valtio 是啥玩意

Valtio makes proxy-state simple for React and Vanilla

就是让数据管理在 React 和原生 JS (Vanilla) 中变得更加简单的一个库,它类似于 Vue 的数据驱动视图的理念,使用外部状态代理去驱动 React 视图来更新。总的来说,Valtio(用粤语来念就是“我丢”) 是一个很轻量级的响应式状态管理库。

2.主要作者是谁?

主要作者叫做 Daishi Kato(带师?是你吗?)他是日本东京人,是个全职开源作者。戳多马蝶,这货居然还写了好几个状态管理库,分别是 Jotai 13.5k⭐Zustand 31.2k⭐Valtio 7k⭐ ,这三个状态管理库都是这货主要开发的,而且用的人还挺多的。其中 Jotai 和 Recoil 类似, Zustand 和 Redux 类似,Valtio 和 Mobx 类似,它们的名字分别是日语、 德语、芬兰语 中的 “状态”,这几个库和之前一些老牌的库比上手要更简单,而且使用起来更简洁,并且主打轻量级。

上面提到的几个库本质上代表了3个流派:

dispatch 流派(单向数据流-中心化管理):redux、zustand、dva等

响应式流派(中心化管理):mobx、valtio等

原子状态流派(原子组件化管理):recoil、jotai等

下面我们来举几个关于上面提到的 zustand、jotai 、valtio 的基本使用例子,对这几个库有个整体的感知,以计时器为例:

Zustand

import { create } from "zustand";

const useStore = create((set) => ({
  count: 0,
  inc: () => set((state) => ({ count: state.count + 1 })),
}));

export default function Counter() {
  const count = useStore((state) => state.count);
  const inc = useStore((state) => state.inc);

  return (
    <div>
      {count}
      <button onClick={inc}>+1</button>
    </div>
  );
}

Jotai

每个状态都是原子化,用法和原生的 useState 有点像

import { atom, useAtom } from "jotai";

const countAtom = atom(0);

function Counter() {
  const [count, setCount] = useAtom(countAtom);

  return (
    <div>
      {count}
      <button onClick={() => setCount((v) => v + 1)}>+1</button>
    </div>
  );
}

Valtio

和 Vue 的响应式类似,当数据发生变化的时候就驱动视图更新

import { proxy, useSnapshot } from "valtio";

const state = proxy({ count: 0 });

function Counter() {
  const snap = useSnapshot(state);

  return (
    <div>
      {snap.count}
      <button onClick={() => ++state.count}>+1</button>
    </div>
  );
}

用三个简单的计时器例子看完了它们三者之间的代码风格差异。

关于如何选择完全是要看个人风格喜好了,我个人的话更喜欢响应式风格的,因为我以前写过一年的Vue,而且 Mobx 我也在之前项目中用过很长的一段时间了,所以 Valtio 就觉得很亲切。但是响应式风格和 React 的单向数据流理念有点违背,所以用户没有 dispatch 流派用的人那么多(从⭐ 的数量就能看出来)。

我们今天这里的主角是 Valtio,下面就讲讲 Valtio 的使用

3.基础:如何使用

从上面的例子中我们可以看到 Valtio 最主要的两个 API 是 proxy 和 useSnapshot,proxy 会为原始对象创建一个 Proxy 代理。使用 useSnapshot 会创建一个组件中的本地快照 snap,并且这个快照是只读的(readonly),当改变 state.count 时,该组件就会被重新渲染,但是改变 state.text 的值时,组件不会重渲染,这里的渲染过程经过优化的。

由于底层和 Vue3 一样使用了 Proxy 来做为数据代理,所以我们先看看它的兼容性,可以看到除了 IE 不支持以外别的浏览器都支持得很好了。

【Valito入门】一个很好用的React响应式状态库

监听数据变化

用于监听数据变化时,valtio 提供了 subscribe 这个 API,下面我们看看效果和代码实现

效果演示

【Valito入门】一个很好用的React响应式状态库

示例代码

import { useEffect } from "react";
import { proxy, subscribe, useSnapshot } from "valtio";

const state = proxy({
  count: 0,
  test: {
    arr: [] as string[],
  },
});

// 也可以在组件外部或任意地方去监听数据的变化,并且可以只监听其中的某一个对象类型的值
subscribe(state.test.arr, () => {
  console.log("在外部监听到 state.test.arr 发生变化了", state.test.arr);
});

export default function Counter() {
  const snap = useSnapshot(state);

  useEffect(() => {
    const unSubscribe = subscribe(state, () => {
      // 此处可以拿到最新的数据
      console.log("在组件内监听到 state 发生变化了", state.count);
    });

    return () => {
      unSubscribe();
    };
  }, []);

  console.log("re-render");

  return (
    <>
      <button
        onClick={() => {
          // 同时修改多个状态,组件也只会 re-render 一次
          state.count += 1;
          state.test.arr.push(String(state.count));
        }}
      >
        do it
      </button>
      <div>{snap.count}</div>
      {snap.test.arr.map((i, k) => (
        <div key={k}>{i}</div>
      ))}
    </>
  );
}

如果需要监听多个属性的变化,可以使用从 valtio/utils 里导出的 watch API,和 Vue 的 API 有点类似

异步数据

【Valito入门】一个很好用的React响应式状态库

const sleep = (ms = 3000) => new Promise((resolve) => setTimeout(resolve, ms));

const state = proxy({
  asyncState: sleep().then(() => "异步加载完成"),
});

function AsyncComponent() {
  const snap = useSnapshot(state);
  return <div>{snap.asyncState}</div>;
}

export default function App() {
  return (
    <Suspense fallback="加载中...">
      <AsyncComponent />
    </Suspense>
  );
}

snapshot 取消代理

可以将一个用 proxy 包裹代理过的可变对象还原成一个不可变的对象。

简单地说,在顺序的快照调用中,当代理对象的值没有改变时,将返回一个指向相同的前一个快照对象的指针。这可以在函数组件中进行浅比较来避免重渲染。这个函数对于我们后面理解原理比较重要,下面是个使用例子:

import { proxy, snapshot } from 'valtio'

const store = proxy({ name: 'Puff' })
// 返回一个当前代理 store 的有效复制,并且取消了 proxy 代理
const snap1 = snapshot(store) 
const snap2 = snapshot(store)
// true,因为 store 中的值没发生改变,所以不需要重渲染
console.log(snap1 === snap2)

// 改变 store 中的值
store.name = 'PuffMeow'
const snap3 = snapshot(store)
// 返回 false,应该进行重渲染
console.log(snap1 === snap3)

几个使用时的注意事项

1.啥时候用 snap 啥时候用 state

在 React 函数组件中, snap 应该和 hooks 一样,只在渲染体(render-body)里去用,state 应该在非渲染体里去用。这钟写法看起来有一点点割裂,不过在下面的最佳实践部分,我们会用另一种方式去解决这种割裂的写法。

import { proxy, useSnapshot } from "valtio"

const state = proxy({
  count: 0
})

const Component = () => {
  const snap = useSnapshot(state)
  // 这里是 render-body

  const handleClick = () => {
    // 这里是非 render-body
      
    state.count++
    // 在这里读取 snap 会获取到老的状态
    console.log(snap) // count: 0
    // 在这里获取 state 可以获取到最新状态
    console.log(state) // count: 1
  }
  
  return <button onClick={handleClick}>+1</button>
}

2.访问整个对象时任意属性触发都会导致重渲染

假如我们有这么一个对象

const state = proxy({
  obj: {
    count: 0,
    text: "hello world"
  }
})

当我们用 snap 去获取 count 时,组件只会在 count 发生变化时才会重新渲染

const snap = useSnapshot(state)
snap.obj.count

但是假如我们在组件内获取 obj ,那么当 obj 发生变化时,不管是 count 变化还是 text 变化,都会让组件触发重渲染

const snap = useSnapshot(state)
snap.obj

// 或者
const snapObj = useSnapshot(state.obj)
snapObj

所以我们应该在渲染体内尽量的精确读取某一个对象里的属性,防止不必要的 re-render

3. 传递对象属性给 React.memo 包裹的组件可能会引发问题

useSnapshot 返回的 snap 变量是用来做重渲染优化数据追踪的,如果你把整个 snap 或者 snap 嵌套的对象属性传递给一个 React.memo 包裹着的组件,可能会有问题,因为 memo 只会做浅层比较来决定是否重渲染一个组件,如果是遇到嵌套对象的话,那么 memo 就会失效了

下面是一些开发时的约定:

  • 不要传递一个对象属性给 React.memo 包裹的组件

  • 要传递对象的时候就避免使用 React.memo

  • 如果非要传递对象给 React.memo 包裹的组件的话,可以传递 proxy 代理过的对象,子组件里使用 useSnapshot 去读取

const state = proxy({
  obj: [
    { id: 1, label: 'foo' },
    { id: 2, label: 'bar' },
  ],
})

const Parent = React.memo(() => {
  const stateSnap = useSnapshot(state)

  return stateSnap.obj.map((item, index) => (
    <Child key={item.id} objectProxy={state.obj[index]} />
  ))
})

const Child = React.memo(({ objectProxy }) => {
  const objectSnap = useSnapshot(objectProxy)

  return objectSnap.label
})

最佳代码实践

在代码中一般我会这样去管理一个全局数据,这也是官方推荐的写法,使用 useProxy 来封装一个获取全局 store 数据的自定义 hook useStore 即可,这样获取数据和设置数据的时候都可以使用 store 这个变量名来设置,避免了上面提到的 snap 和 store 割裂的写法。

// src/store/index.ts
import { useProxy, proxy } from "valtio/utils";

const store = proxy({
    userInfo: {},
    list: []
})

// 定义一个取数据的 hooks
export default useStore = () => useProxy(store);

// src/components/List 组件
import useStore from "../../store";

export function List() {
    const store = useStore();
    
    useEffect(() => {
        fetchData.then(res => {
            store.list = res.data.list;
            // 这里读取 store.list 可以取到最新的值
        })
    }, [])
    
    return (
        <div>
            {store.list.map(item => <div>{item.name}</div>)}
        </div>
    )
}

useProxy 其实就是对取 useSnapshot() 或 store 数据的封装,这个 hook 也很简单,就是判断是渲染期间(渲染体内)就返回 useSnapshot() 的快照数据,非渲染期间(非渲染体内)就返回原始的 store 数据,和我们自己手写的是差不多的,只不过这个 hook 帮我们把这个过程封装了起来。

小结

以上就是关于 Valtio 库的基本使用了,使用起来的感受和 Vue 的响应式比较像,都是收集依赖到触发依赖更新的一个过程,内部都使用了 Proxy 进行代理。目前在公司内部我也有项目在用,总的来说,小项目使用起来还是挺方便的,但是大型项目的话如果稍微用不好那可能就会掉坑里去,不过总的来说,还是很好用的~

本篇文章同时收录在公众号《泡芙学前端》,持续更新内容中,欢迎关注~

转载自:https://juejin.cn/post/7238027797313683493
评论
请登录