并不全局的全局存储
在 web 开发的学习中,你也许都已经学习过 vue 的 pinia,react 的 redux/dva/jotai/zustand 等存储方法。
他们都教你创建一个可以全局访问的的对象,并去全局读取/写入他们。但是在 H5 开发中,我们有很多这样的场景。例如下面的抖音界面
【视频瀑布流】-【个人主页】-【视频瀑布流】
两个甚至多个视频瀑布流使用了完全相同的组件,但是他们读取的数据是完全不同的, 你可能想象只需要传递不同的 props 就行了!
但是在移动端应用中,瀑布流几乎是最复杂的场景,他们一个组件可能包括如下业务
- 虚拟列表
- 瀑布流布局
- 曝光检查
- 双向无限滚动加载
- 动态插入数据
这样的场景下很难不用一个 store 去管理住列表的数据,每个深层组件都有可能对列表进行更新操作。
这种情况你可能想到 context,如果在 vue 中你就无需担心那么多,但在 react 中 context 中每个字段刷新都会导致子组件刷新。
因此我们可以有个更好的实现:把 store 对象放进 context 里!store 本身是一个不可变对象,但是其内部变化会让组件内部订阅者更新。
设计过程
我们以 zustand 为例
下面这个包很好的解决了这样的问题,他通过上下文来创建多个 store
我们来简单 复原一份
export const useTestStore = createStore<Store>((set) => ({
color: "#ff0000",
setColor: (color: string) => set({ color }),
}));
const Component = () => {
const color = useTestStore((s) => s.color);
return <div>{color}</div>;
};
我们原本创建存储的时候,都直接创建了存储本身,但是现在我们需要多份存储实例。
于是我们改成函数的方法,每次调用的时候产生一个实例
import { createStore, useStore } from "zustand";
import { createContext, useContext, useRef } from "react";
const TestStoreBuilder = (initColor?: string) =>
createStore<Store>((set) => ({
color: initColor || "#ff0000",
setColor: (color: string) => set({ color }),
}));
const TestStoreContext = createContext<ReturnType<
typeof TestStoreBuilder
> | null>(null);
export const TestStoreContainer = (props: { children: ReactNode }) => {
const { children } = props;
// 更好的做法是 const [storeRef] = useState(TestStoreBuilder) // 这样不会重复调用 TestStoreBuilder
const storeRef = useRef(TestStoreBuilder());
return (
<TestStoreContext.Provider value={storeRef.current}>
{children}
</TestStoreContext.Provider>
);
};
export function useTestStore<T>(selector: (state: Store) => T) {
const store = useContext(TestStoreContext);
if (!store) {
throw new Error("Missing Provider");
}
return useStore(store, selector);
}
export const Component = () => {
// 注意在这里面 useColorStore,会报错,因为不在上下文中
return (
<TestStoreContainer>
<Content />
</TestStoreContainer>
);
};
const Content = () => {
// 在这里面 useColorStore,和直接使用刚才的体验是一致的
const color = useTestStore((s) => s.color);
return <div>{color}</div>;
};
上面的方法不够通用!改成通用的函数。
通用化
export function createProviderStore<StoreType>(
StoreBuilder: (initState: DeepPartial<StoreType>) => StoreApi<StoreType>,
) {
type StoreInstance = ReturnType<typeof StoreBuilder>;
const StoreContext = createContext<StoreInstance | undefined>(undefined);
interface ProviderProps {
children: ReactNode;
/** 传入初始化该store的内容,结构与store一致 */
init?: Partial<StoreType>;
}
const StoreProvider = ({ children, init = {}}: ProviderProps) => {
const storeRef = useRef<StoreInstance | null>(null);
if (!storeRef.current) {
storeRef.current = StoreBuilder(init);
}
return <StoreContext.Provider value={storeRef.current!}>{children}</StoreContext.Provider>;
};
function useFactoryStore<T>(selector: (store: StoreType) => T): T {
const store = useContext(StoreContext);
if (!store) {
throw new Error(`Missing Provider!`);
}
return useStore(store, selector)
}
return { StoreProvider, useFactoryStore };
}
这样,对于全部 StoreBuilder 都可以通过这个函数直接创建上下文了
相比直接食用 zustand 的 store,这里还把导出的 useFactoryStore(selector)参数改为必传了,解决很多忘传 selector 导致组件频繁刷新的问题
此处还新增了 init 的参数的支持,如果你写过 ssr 相关的代码,你会知道状态初始值有多重要
使用案例
export const {
StoreProvider: TestColorProvider,
useFactoryStore: useColorTest,
} = createProviderStore(TestStoreBuilder);
const Page = () => {
return (
<TestColorProvider init={{ color }}>
<Content />
</TestColorProvider>
);
};
const Content = () => {
const color = useColorTest((s) => s.color);
return color;
};