Jotai 实战(上)

·

2 min read

早前,我们介绍了《从 RecoilJotai》 的迁移。其实随着迁移的过程中,也有着一些实践性的好方法,这里做一个总结提炼。

文章中的代码地址:github.com/bigbigDreamer/jotai-BPs 快捷传送门

大纲

  1. atom 细粒度作用域控制

  2. atom 懒请求

  3. atom 本地缓存

  4. atom 动态 key

1. atom 细粒度作用域控制

目的:实现不同层级的状态管理,例如:组件层页面层应用层 的细粒度控制。

实现方案

在不同的作用域层级添加 Provider ,作为隔离效果,还有一个好处是:当重新加载时,挂在 Provider 的所有 Atom 会被重置状态。后者才是最为好用的一个能力。

import { Provider } from 'jotai'

const Component: FC = ({ children }) => <Provider>{ children }</Provider>

2. atom 懒请求

目的:在一个方法中,同时做到 atomgetset ,避免写过多的样板代码。

这里写了一个 demo ,大概就是每次滚动到底部时,去主动触发一次请求。

实现方案

这里仅列举最核心的伪代码,减轻阅读负担。

// some code
const loadMoreData = useAtomCallback(useCallback(async (get, set) => {  
    const _data = get(globalData);  
    await sleep(1500);  
    set(globalData, {data: [..._data.data, ...genUsers()]})  
    message.success("data load success!")  
}, []));  

useEffect(() => {  
    loadMoreData()  
}, [])

可以看到,我们借助了 useAtomCallback 来实现这个能力,而 useCallback 的作用是为了维持 useAtomCallback 内部的函数引用唯一,避免加载 死循环

重点:使用 useAtomCallback 时,必须在整个组件上下文中去 subscribe atom ,简单点就是 useAtom 或者 useAtomValue,否则无法 get 最新的内容。

3. atom 本地缓存

目的:实现 本地缓存atom 结合。

这里写了一个 demo ,目的是为了将 二维码 的地址,持久化到 IndexDB ,同时满足 f(UI) 的需要。

实现方案

store.ts

我们借助 localforage 来实现降级,但是内部我们做了最终降级,假设 IndexDBwebSQLLocalStorage 都不支持,我们最终降级到 memory map 中,保证程序运转是正常的。

import { atomWithStorage } from 'jotai/utils';  
import localforage from "localforage";  
import to from 'await-to-js';  

const localforageInstance = localforage.createInstance({  
    storeName: 'LocalPageStore',  
    version: 1  
})  
const memoryCache = new Map();  
const localForageStore: Parameters<typeof atomWithStorage>[2] = {  
    async setItem(key, value) {  
        const [err] =  await to(localforageInstance.setItem(key, value));  
        if(err) {  
            memoryCache.set(key, value);  
        }  
    },  
    async getItem(key) {  
        const [err, result] =  await to(localforageInstance.getItem(key));  
        if(err) {  
            return memoryCache.get(key) || null;  
        }  
        return  result  
    },  

    async removeItem(key) {  
        const [err] =  await to(localforageInstance.removeItem(key));  
        if(err) {  
            memoryCache.delete(key);  
        }  
    }  
}  

export const pageStorageStore = atomWithStorage('LocalCache', null, localForageStore)

4. atom 动态 key

目的:实现一个 atom 引用,可以作用多个 atom 的读写。

这里写了一个 demo ,拆分两个 atom ,一个控制 checked ,一个控制 文案的显示

实现方案

store.ts

import { atomFamily } from 'jotai/utils';  
import {WritableAtom, atom} from "jotai";  

const keyMapAtom: {  
    [StoreKey.CHECK_STORE]: WritableAtom<boolean, boolean[], void>;  
    [StoreKey.CONTENT_STORE]: WritableAtom<string, string[], void>  
} = {  
    CHECK_STORE: atom(false),  
    CONTENT_STORE: atom('')  
}  
export const pageStore = atomFamily<StoreKey, WritableAtom<boolean, boolean[], void> | WritableAtom<string, string[], void>>(key => keyMapAtom[key]);  

export enum StoreKey {  
    CHECK_STORE = 'CHECK_STORE',  
    CONTENT_STORE = 'CONTENT_STORE'  
}

index.tsx

// checked atom
const [checked, setChecked] = useAtom<boolean, [boolean], void>(pageStore(StoreKey.CHECK_STORE) as WritableAtom<boolean, boolean[], void>) 
// content atom
const [content, setContent] = useAtom<string, [string], void>(pageStore(StoreKey.CONTENT_STORE) as WritableAtom<string, string[], void>)

const handleCheck = () => {  
    setChecked(!checked)  
    if(checked) {  
        setContent("Not Checked")  
    } else {  
        setContent("Checked")  
    }  
}  

const checkContainerCls = classes('check-container', {  
    'check-container-switch': checked  
})  
// memory GC
useEffect(() => () => {  
    pageStore.setShouldRemove(() => true);  
})

我们可以借助 atomFamily 来实现 paramsatom ,这其实对于 fetch atom 很友好,可以实现不同参数的 fetch,尤其是 url params 请求。

但是 atomFamily 我们在 下篇介绍过,会存在内存泄漏的风险,因为内部使用了 Map 作为不自动回收的存储数据结构。

所以我们借助了 setShouldRemove 在页面 卸载 时,进行全量清理。


好了,Jotai BPs 上篇 到这里就结束了。

我是 不换,如果写的不正确,欢迎评论区批评指正,如果对你有帮助,请点个小赞,赠人玫瑰,手有余香。我们下期再见👋。