Jotai 基于IndexedDB的持久化

245 阅读1分钟

喜欢用Zustand和Jotai等工具搭配MMKV or IndexedDB等轻量的存储工具,封装前端的Model层,然后将其作为数据库使用,业务代码都封入其中,这是一个持久化的极简实践

截屏2025-01-15 10.42.27.png

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>
  );
}

git地址

Jotai with IndexedDB