持久化中间件

持久中间件使你能够将 Zustand 状态存储在storage中(例如localStorage, AsyncStorage, IndexedDB等),从而持久化它的数据。
请注意,此中间件确实支持同步存储(例如 localStorage)和异步存储(例如 AsyncStorage),但使用异步存储确实会带来成本。
有关更多详细信息,请参阅 水合作用和异步存储

快速示例:

import create from "zustand"
import { persist } from "zustand/middleware"

export const useStore = create(persist(
  (set, get) => ({
    fishes: 0,
    addAFish: () => set({ fishes: get().fishes + 1 })
  }),
  {
    name: "food-storage", // name of item in the storage (must be unique)
    getStorage: () => sessionStorage, // (optional) by default the 'localStorage' is used
  }
))

请参阅选项了解更多详情

选项

name

这是唯一的必填选项选项。
给定的名称将是用于在store中存储你的 Zustand 状态的键,因此它必须是唯一的。

getStorage

使你能够使用自己的storage。
只需传递一个函数,该函数返回你要使用的store。

示例:

export const useStore = create(persist(
  (set, get) => ({
    // ...
  }),
  {
    // ...
    getStorage: () => AsyncStorage,
  }
))

给定的storage必须匹配以下接口:

interface Storage {
  getItem: (name: string) => string | null | Promise<string | null>
  setItem: (name: string, value: string) => void | Promise<void>
  removeItem: (name: string) => void | Promise<void>
}

serialize

由于将对象存储在storage中的唯一方法是通过字符串,你可以使用此选项提供自定义函数以将你的状态序列化为字符串。

例如,如果你想将状态存储在 base64 中:

export const useStore = create(persist(
  (set, get) => ({
    // ...
  }),
  {
    // ...
    serialize: (state) => btoa(JSON.stringify(state)),
  }
))

请注意,你还需要一个自定义deserialize (反序列化)函数才能使其正常工作。见下文。

deserialize

如果你传递自定义序列化函数,则很可能还需要传递自定义反序列化函数。
要继续上面的示例,你可以使用以下命令反序列化 base64 值:

export const useStore = create(persist(
  (set, get) => ({
    // ...
  }),
  {
    // ...
    deserialize: (str) => JSON.parse(atob(str)),
  }
))

partialize

使你能够排除一些要存储在storage中的状态字段。
你可以使用以下方法排除多个字段:

export const useStore = create(persist(
  (set, get) => ({
    foo: 0,
    bar: 1,
  }),
  {
    // ...
    partialize: (state) =>
      Object.fromEntries(
        Object.entries(state).filter(([key]) => !["foo"].includes(key))
      ),
  }
))

或者你可以只允许使用如下的特定字段:

export const useStore = create(persist(
  (set, get) => ({
    foo: 0,
    bar: 1,
  }),
  {
    // ...
    partialize: (state) => ({ foo: state.foo })
  }
))

onRehydrateStorage

此选项使你能够传递将在存储storage水合时调用的侦听器函数。

示例:

export const useStore = create(persist(
  (set, get) => ({
    // ...
  }),
  {
    // ...
    onRehydrateStorage: (state) => {
      console.log("hydration starts");

      // optional
      return (state, error) => {
        if (error) {
          console.log("an error happened during hydration", error)
        } else {
          console.log("hydration finished")
        }
      }
    }
  }
))

version

如果要在storage中引入重大更改(例如重命名字段),可以指定新版本号。
默认情况下,如果storage中的版本与代码中的版本不匹配,则存储的值不会被使用。
有关处理重大更改的更多详细信息,请参阅下面的migrate选项。

migrate

你可以使用此选项来处理版本迁移。
migrate 函数将持久化状态和版本号作为参数。它必须返回符合最新版本(代码中的版本)的状态。
例如,如果要重命名字段,可以使用以下命令:

export const useStore = create(persist(
  (set, get) => ({
    newField: 0, // let's say this field was named otherwise in version 0
  }),
  {
    // ...
    version: 1, // a migration will be triggered if the version in the storage mismatches this one
    migrate: (persistedState, version) => {
      if (version === 0) {
        // if the stored value is in version 0, we rename the field to the new name
        persistedState.newField = persistedState.oldField;
        delete persistedState.oldField;
      }

      return persistedState;
    },
  }
))

merge

在某些情况下,你可能希望使用自定义合并函数将持久值与当前状态合并。
默认情况下,中间件进行浅合并。
如果你部分持久化了嵌套对象,那么浅层合并可能还不够。
例如,如果storage包含以下内容:

{
  foo: {
    bar: 0,
  }
}

但是你的 Zustand store包含:

{
  foo: {
    bar: 0,
    baz: 1,
  }
}

浅合并将从foo对象中删除 baz字段。
解决此问题的一种方法是提供自定义深度合并功能:

export const useStore = create(persist(
  (set, get) => ({
    foo: {
      bar: 0,
      baz: 1,
    },
  }),
  {
    // ...
    merge: (persistedState, currentState) => deepMerge(currentState, persistedState),
  }
))

API

持久化api使你能够从 React 组件的内部或外部与持久中间件进行大量交互。

setOptions

此方法使你能够更改中间件选项。请注意,新选项将与当前选项合并。

例如,这可用于更改storage名称:

useStore.persist.setOptions({
  name: "new-name"
});

甚至更改storage引擎:

useStore.persist.setOptions({
  getStorage: () => sessionStorage,
});

clearStorage

这个方法用于完全清除storage中的持久值。

useStore.persist.clearStorage();

rehydrate

在某些情况下,你可能希望手动触发水合作用。
这可以通过调用 rehydrate方法来完成。

await useStore.persist.rehydrate();

hasHydrated

这是一个非响应式的 getter,用于了解storage是否已被水合(请注意,这在调用 useStore.persist.rehydrate() 时会更新)。

useStore.persist.hasHydrated();

onHydrate

水合过程开始时将调用给定的监听器。

const unsub = useStore.persist.onHydrate((state) => {
  console.log("hydration starts");
});

// later on...
unsub();

onFinishHydration

当水合过程结束时,将调用给定的监听器。

const unsub = useStore.persist.onFinishHydration((state) => {
  console.log("hydration finished");
});

// later on...
unsub();

水合和异步存储

要解释异步存储的“成本”是什么,你需要了解什么是水合作用。
简而言之,水合是从storage中检索持久状态并将其与当前状态合并的过程。
persist 中间件执行两种 hydration:同步和异步。
如果给定的storage是同步的(例如localStorage),则 hydration 将同步完成,如果给定的存储是异步的(例如 AsyncStorage),则 hydration 将异步完成......🥁。
但问题是什么?
好吧,在同步水合中,Zustand storage将在其创建时进行水合。
在异步水合中,Zustand storage将在稍后的微任务中水合。
为什么这有关系?
异步水合可能会导致一些意外行为。
例如,如果你在 React 应用程序中使用 Zustand,store将不会在初始渲染时加水。如果你的应用程序依赖于页面加载时的持久值,你可能希望等到store已被水合后再显示任何内容(例如,你的应用可能认为用户未登录,因为这是默认值,而实际上store还没有被水合)。
如果你的应用确实依赖于页面加载时的持久状态,请参阅如何检查我的store是否已加水?在 Q/A 中。

Q/A

如何检查我的storage是否已加水?

有几种不同的方法可以做到这一点。
你可以使用 onRehydrateStorage 选项来更新store中的字段:

const useStore = create(
  persist(
    (set, get) => ({
      // ...
      _hasHydrated: false
    }),
    {
      // ...
      onRehydrateStorage: () => () => {
        useStore.setState({ _hasHydrated: true })
      }
    }
  )
);

export default function App() {
  const hasHydrated = useStore(state => state._hasHydrated);

  if (!hasHydrated) {
    return <p>Loading...</p>
  }

  return (
    // ...
  );
}

你还可以创建自定义使用 Hydration hook:

const useStore = create(persist(...))

const useHydration = () => {
  const [hydrated, setHydrated] = useState(useStore.persist.hasHydrated)

  useEffect(() => {
    const unsubHydrate = useStore.persist.onHydrate(() => setHydrated(false)) // Note: this is just in case you want to take into account manual rehydrations. You can remove this if you don't need it/don't want it.
    const unsubFinishHydration = useStore.persist.onFinishHydration(() => setHydrated(true))

    setHydrated(useStore.persist.hasHydrated())

    return () => {
      unsubHydrate()
      unsubFinishHydration()
    }
  }, [])

  return hydrated
}

如何使用自定义storage引擎

如果你要使用的storage与预期的 API 不匹配,你可以创建自己的存储:

import create from "zustand"
import { persist, StateStorage } from "zustand/middleware"
import { get, set, del } from 'idb-keyval' // can use anything: IndexedDB, Ionic Storage, etc.

// Custom storage object
const storage: StateStorage = {
  getItem: async (name: string): Promise<string | null> => {
    console.log(name, "has been retrieved");
    return (await get(name)) || null
  },
  setItem: async (name: string, value: string): Promise<void> => {
    console.log(name, "with value", value, "has been saved");
    await set(name, value)
  },
  removeItem: async (name: string): Promise<void> => {
    console.log(name, "has been deleted");
    await del(name)
  }
}

export const useStore = create(persist(
  (set, get) => ({
    fishes: 0,
    addAFish: () => set({ fishes: get().fishes + 1 })
  }),
  {
    name: "food-storage", // unique name
    getStorage: () => storage,
  }
))

发表评论

您的电子邮箱地址不会被公开。