Zustand:狀態持久化在專案中的應用

一秋知叶發表於2024-08-25

Zustand的持久化中介軟體允許你將狀態儲存在各種儲存中,例如localStorageAsyncStorageIndexedDB等。這使得應用的狀態可以跨頁面持久化。也就是說使用者重新整理頁面或者關閉瀏覽器後重新開啟,應用的狀態依然可以被保留。

使用方法

首先,你需要從zustand庫中匯入createpersist函式,以及createJSONStorage輔助函式。

import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';

然後,使用persist函式包裝你的Zustand store,並提供必要的配置。

下面是一個簡單的示例,演示瞭如何建立一個持久化的Zustand store:

export const useBearStore = create(
  persist(
    (set, get) => ({
      bears: 0,
      addABear: () => set({ bears: get().bears + 1 }),
    }),
    {
      name: 'bear-storage', // 儲存中的專案名稱,必須是唯一的
      storage: createJSONStorage(() => sessionStorage), // 使用sessionStorage作為儲存
    },
  ),
);

持久化選項

以下是一些常用的持久化選項:

  • name:儲存中使用的唯一鍵名。
  • storage:自定義儲存引擎。
  • partialize:選擇儲存部分狀態欄位。
  • onRehydrateStorage:儲存恢復時呼叫的監聽函式。
  • version:版本控制,用於處理儲存的相容性。
  • migrate:處理版本遷移的函式。
  • merge:自定義持久化值與當前狀態的合併方式。
  • skipHydration:跳過初始化時的自動恢復。

版本控制與遷移

如果你的狀態結構發生變化,比如欄位重新命名或新增欄位,你可以使用versionmigrate選項來處理:

export const useVersionedStore = create(
  persist(
    (set, get) => ({
      newField: 0,
    }),
    {
      name: 'versioned-storage',
      version: 1,
      migrate: (persistedState, version) => {
        if (version === 0) {
          persistedState.newField = persistedState.oldField;
          delete persistedState.oldField;
        }
        return persistedState;
      },
    },
  ),
);

手動觸發恢復

在某些情況下,你可能需要手動觸發狀態的恢復,可以使用rehydrate方法:

await useBoundStore.persist.rehydrate();

檢查是否已恢復

使用hasHydrated方法可以檢查狀態是否已經恢復:

const hasHydrated = useBoundStore.persist.hasHydrated();

案例實踐

這裡分享一個開源專案ChatGPT-Next-Web中的使用(https://github.com/Yidadaa/ChatGPT-Next-Web),這也是一個寶藏專案,可以快速搭建一個自己的GPT。專案中zustand用於全域性的狀態管理,主要用於儲存GPT對話聊天資訊。

工具方法,createPersistStore

裡面所有的狀態store都是透過該Util方法建立的。

export function createPersistStore<T extends object, M>(
  state: T,
  methods: (
    set: SetStoreState<T & MakeUpdater<T>>,
    get: () => T & MakeUpdater<T>,
  ) => M,
  persistOptions: SecondParam<typeof persist<T & M & MakeUpdater<T>>>,
) {
  return create(
    persist(
      combine(
        {
          ...state,
          lastUpdateTime: 0,
        },
        (set, get) => {
          return {
            ...methods(set, get as any),

            markUpdate() {
              set({ lastUpdateTime: Date.now() } as Partial<
                T & M & MakeUpdater<T>
              >);
            },
            update(updater) {
              const state = deepClone(get());
              updater(state);
              set({
                ...state,
                lastUpdateTime: Date.now(),
              });
            },
          } as M & MakeUpdater<T>;
        },
      ),
      persistOptions as any,
    ),
  );
}

這個函式,在建立store的時候,新增了一個記錄更新時間的欄位,並在資料更新的時候自動更新時間。同時也支援手動觸發標記更新。

引數:

  • state: 初始狀態物件。
  • methods: 一個函式,接收set和get方法,返回一些自定義的方法。這些方法會被合併到最終的store中。
  • persistOptions: 持久化的選項,這些選項將被傳遞給persist中介軟體。

函式體:

  • 使用zustand的create函式建立一個store。
  • 在create函式內部,首先使用persist和combine中介軟體。
  • combine中介軟體用於合併多個store或提供額外的方法。在這裡,它合併了初始狀態和由methods函式提供的方法。
  • 在combine的回撥函式內部,除了透過methods函式提供的方法外,還額外新增了兩個方法:markUpdate和update。
    • markUpdate方法:更新lastUpdateTime欄位為當前時間。
    • update方法:深複製當前狀態,對複製後的狀態應用更新函式,然後設定新的狀態和lastUpdateTime。
  • 最後,將持久化選項persistOptions傳遞給persist中介軟體。

combine 是 zustand 提供的一個工具函式,用於組合狀態和行為方法。它將狀態和行為方法組合成一個新的物件,這樣你就可以使用 set 和 get 方法來更新和訪問狀態。

具體來說,combine 接受兩個引數:

  • 1.初始狀態(上面的程式碼中的 state 和 lastUpdateTime)
  • 2.行為方法(一個函式,接收 set 和 get 引數,並返回一些方法,比如 markUpdate 和 update)

這個組合讓你可以把狀態和方法結合在一起,然後傳遞給 persist,最終建立出一個帶有持久化功能的 store。

使用方式

有了工具函式,就可以用來建立具體的store了。以下是其中使用者配置資料的store,其他的也差不多,主要是store業務方法的完善。

export const useAppConfig = createPersistStore(
  { ...DEFAULT_CONFIG },
  (set, get) => ({
    reset() {
      set(() => ({ ...DEFAULT_CONFIG }));
    },

    mergeModels(newModels: LLMModel[]) {
      if (!newModels || newModels.length === 0) {
        return;
      }

      const oldModels = get().models;
      const modelMap: Record<string, LLMModel> = {};

      for (const model of oldModels) {
        // model.available = false;
        modelMap[`${model.name}@${model?.provider?.id}`] = model;
      }

      for (const model of newModels) {
        // model.available = Boolean(newModels.available);
        modelMap[`${model.name}@${model?.provider?.id}`] = model;
      }

      set(() => ({
        models: Object.values(modelMap),
      }));
    },

    allModels() {},
  }),
  {
    name: StoreKey.Config,
    version: 3.9,
    migrate(persistedState, version) {
      const state = persistedState as ChatConfig;

      if (version < 3.4) {
        state.modelConfig.sendMemory = true;
        state.modelConfig.historyMessageCount = 4;
        state.modelConfig.compressMessageLengthThreshold = 1000;
        state.modelConfig.frequency_penalty = 0;
        state.modelConfig.top_p = 1;
        state.modelConfig.template = DEFAULT_INPUT_TEMPLATE;
        state.dontShowMaskSplashScreen = false;
        state.hideBuiltinMasks = false;
      }

      if (version < 3.5) {
        state.customModels = "claude,claude-100k";
      }

      return state as any;
    },
  },
);

可以看到,這裡主要是這幾件事

  • 把初始狀態傳進去
  • 增加一些store的處理方法:mergeModels、reset
  • 定義了遷移策略

在這個專案中,我們也遇到了一個問題,在支援圖片之後,儲存的聊天記錄裡,很輕易地就超過了5M,因為GPT本質上是不支援檔案的,只支援base64,聊天記錄裡有base64,導致幾個來回之後就超過了5M。然後寫入失敗導致應用無法正常工作。

所以我們做了一個調整,寫入前,先判斷一下大小,過大則淘汰掉最老的那個記錄。這種處理,在狀態管理中實現就很輕鬆了,也不用用太大的心理顧慮。

結論

Zustand的持久化功能為React應用的狀態管理提供了強大的支援,使得狀態可以跨頁面甚至跨會話持久化。透過上述示例,你應該能夠理解如何在你的應用中實現狀態的持久化。記得根據你的具體需求選擇合適的儲存引擎和配置選項。

如果你有任何問題或想要了解更多關於Zustand的資訊,請訪問Zustand官方文件

本文由mdnice多平臺釋出

相關文章