前言
最新在'深入'学习 react reRender 机制时发现除了基本的 setState 触发 reRender(props 本质上也是 setState 后的 reRender),还有第三方库比如各大百花齐放神魔乱舞的公共状态库也能通知到 react 去 reRender 对应的 ui。 于是在好奇心的驱使下去查看现代的公共状态库是怎么能不触发 setState 还可以通知 react 去更新页面,最终我发现了这个被我们大多数人忽略的 hooks: useSyncExternalStore
通过阅读文档可以知道
useSyncExternalStore 是一个让你订阅外部 store 的 React Hook。
具体 api 使用方法之类的可以自行去看文档,简单来说就是:第三方库变化的时候能通知 react 告知它该更新页面了。
之前的 redux 之类的库实现应该是用的 context,通过 setState 来触发 reRender 从而达到更新 ui 的目的,所以这类库都需要通过 Provider 来通知组件更新,而 react 18 新增的useSyncExternalStore 则可以通过更优雅的方式来达到这个效果,所以这也是 react18 以来这么多公共状态库出来的原因吧,百花齐放的时代。
能收获什么?
阅读本篇并不能让你手搓一个类似zustand这样的库出来,只是以最简洁的方式展现 zustand 的基础功能的实现。
开始
声明一个 createStore 函数,这个函数入参是 initFn。同 zustand 的 create 形参,它是一个函数,接收一个 set,
这个 set 用于更新 createStore 的 state 数据。对照 zustand create createStore返回的是一个 useStore 对象,所以他们的类型应该是:
// 定义状态更新函数类型
type SetState<T> = (upater: Partial<T> | ((prev: T) => Partial<T>)) => void;
// createStreo形参类型:接收set 返回初始状态
type CreateStore<T> = (set: SetState<T>) => T;
// createStore返回的hooks类型
type UseStore<T> = <U>(selector: (state: T) => U) => U;
初始化initFn获取创建 store 的数据,创建一个 Set 数据用于触发 set 时通知 react;
export function createStore<T>(initFn: CreateStore<T>): UseStore<T> {
let state: T;
const listeners = new Set<() => void>();
const set: SetState<T> = (updater) => {
const nextState = typeof updater === "function" ? updater(state) : updater;
state = { ...state, ...nextState };
listeners.forEach((l) => l());
};
state = initFn(set);
const useStore: UseStore<T> = (selector) => {
const getSnapshot = () => selector(state);
const subscribe = (callback: () => void) => {
listeners.add(callback);
return () => listeners.delete(callback);
};
return useSyncExternalStore(subscribe, getSnapshot);
};
return useStore;
}
以上就实现了 zustand 的基本功能,如何使用呢? 按照规范肯定是新建一个 store 用于保存公共状态了,新建一个 useCounterStore 来替代 vite 模板里的 count setCount
import { createStore } from "../libs/createStore";
export const useCounterStore = createStore<{
count: number;
inc: () => void;
}>((set) => {
return {
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
};
});
ps:这里有没有大佬告诉我怎么来写 ts 让它自然推到类型而不是我手动写泛型的类型啊。
在 App 页面引入这个状态
import { useCounterStore } from './store'
...
function App() {
const count = useCounterStore((s) => s.count);
const setCount = useCounterStore(s => s.inc)
}
...
<button onClick={() => setCount()}>
count is {count}
</button>
此时触发这个 button click 点击事件就能发现 count 能被正确更新了,还可以自己新建几个路由页面然后使用 count 状态,在切换路由时自行观察。