喜欢用Zustand和Jotai等工具搭配MMKV or IndexedDB等轻量的存储工具,封装前端的Model层,然后将其作为数据库使用,业务代码都封入其中,这是一个持久化的极简实践
store/persit.ts
导出一个createPersistedAtom供外界使用,只需要输入key和初始化数据
import { atom } from 'jotai';
import { get, set, del } from 'idb-keyval';
export const createJSONStorage = () => ({
getItem: async (key: string) => {
const item = await get(key);
return item ? JSON.stringify(item) : null;
},
setItem: async (key: string, value: string) => {
await set(key, JSON.parse(value));
},
removeItem: async (key: string) => {
await del(key);
},
});
export const createPersistedAtom = <T>(key: string, initialValue: T) => {
const storage = createJSONStorage();
const baseAtom = atom(initialValue);
baseAtom.onMount = (setValue) => {
(async () => {
const item = await storage.getItem(key);//在组件挂载时从IndexedDB加载持久化数据
const persistedValue = item !== null ? JSON.parse(item) : initialValue;
if (persistedValue !== initialValue) {
setValue(persistedValue);
}
})();
};
const derivedAtom = atom(
(get) => get(baseAtom),//getter: 返回基础atom的值
(get, set, update) => {//setter: 更新基础atom的值,并将新值持久化到IndexedDB
const nextValue =
typeof update === 'function'
? (update as (value: T) => T)(get(baseAtom))
: update;
set(baseAtom, nextValue);
storage.setItem(key, JSON.stringify(nextValue));
}
);
return derivedAtom;
};
store/index.ts
贡献一个useCounter供页面使用
import { useAtom } from 'jotai';
import { createPersistedAtom } from './persist';
interface CounterState {
count: number;
}
const counterAtom = createPersistedAtom<CounterState>('counter', {
count: 0,
//other fields
});
export const useCounter = () => {
const [state, setState] = useAtom(counterAtom);
return {
count: state.count,
increment: () => setState(v => ({...v, count: v.count + 1})),
decrement: () => setState(v => ({...v, count: v.count - 1})),
reset: () => setState(v => ({...v, count: 0}))
};
};
页面展示
'use client';
import { useEffect, useState } from 'react';
import { useWhyDidYouUpdate } from 'ahooks';
import { useCounter} from "../store/index";
function useCounterDebug(props: any) {
useWhyDidYouUpdate('CounterPage', props);
}
export default function CounterPage() {
const { count, increment, decrement, reset } = useCounter();
useCounterDebug({ count });
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-100">
<div className="bg-white p-8 rounded-lg shadow-lg">
<h1 className="text-3xl font-bold mb-4 text-center">Counter</h1>
<div className="text-center mb-6">
<span className="text-2xl font-semibold">Current Count:</span>
<span
className={`ml-2 text-4xl font-bold ${
count > 0 ? 'text-green-600' :
count < 0 ? 'text-red-600' :
'text-gray-800'
}`}
>
{count}
</span>
</div>
<div className="flex gap-4 justify-center">
<button
onClick={decrement}
className="px-6 py-2 bg-red-500 text-white rounded hover:bg-red-600 transition-colors"
>
Decrement
</button>
<button
onClick={reset}
className="px-6 py-2 bg-gray-500 text-white rounded hover:bg-gray-600 transition-colors"
>
Reset
</button>
<button
onClick={increment}
className="px-6 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
>
Increment
</button>
</div>
</div>
</div>
);
}