【Valito入门】一个很好用的React响应式状态库
本篇文章同时收录在公众号《泡芙学前端》,持续更新内容中,欢迎关注~
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 不支持以外别的浏览器都支持得很好了。
监听数据变化
用于监听数据变化时,valtio 提供了 subscribe 这个 API,下面我们看看效果和代码实现
效果演示
示例代码
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 有点类似
异步数据
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