Skip to content

保持存储数据

¥Persisting store data

Persist 中间件使你能够将 Zustand 状态存储在存储中(例如 localStorageAsyncStorageIndexedDB 等),从而持久保存其数据。

¥The Persist middleware enables you to store your Zustand state in a storage (e.g., localStorage, AsyncStorage, IndexedDB, etc.), thus persisting its data.

请注意,此中间件既支持同步存储(如 localStorage),也支持异步存储(如 AsyncStorage),但使用异步存储需要付出代价。有关更多详细信息,请参阅 补水和异步存储

¥Note that this middleware supports both synchronous storages, like localStorage, and asynchronous storages, like AsyncStorage, but using an asynchronous storage does come with a cost. See Hydration and asynchronous storages for more details.

简单示例

¥Simple example

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

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

Typescript 简单示例

¥Typescript simple example

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

type BearStore = {
  bears: number
  addABear: () => void
}

export const useBearStore = create<BearStore>()(
  persist(
    (set, get) => ({
      bears: 0,
      addABear: () => set({ bears: get().bears + 1 }),
    }),
    {
      name: 'food-storage', // name of the item in the storage (must be unique)
      storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used
    },
  ),
)

选项

¥Options

name

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

¥This is the only required option. The given name is going to be the key used to store your Zustand state in the storage, so it must be unique.

storage

类型:() => StateStorage

¥Type: () => StateStorage

StateStorage 可以通过以下方式导入:

¥The StateStorage can be imported with:

ts
import { StateStorage } from 'zustand/middleware'

默认:createJSONStorage(() => localStorage)

¥Default: createJSONStorage(() => localStorage)

使你能够使用自己的存储。只需传递一个返回你想要使用的存储的函数。建议使用 createJSONStorage 辅助函数创建符合 StateStorage 接口的 storage 对象。

¥Enables you to use your own storage. Simply pass a function that returns the storage you want to use. It's recommended to use the createJSONStorage helper function to create a storage object that is compliant with the StateStorage interface.

示例:

¥Example:

ts
import { persist, createJSONStorage } from 'zustand/middleware'

export const useBoundStore = create(
  persist(
    (set, get) => ({
      // ...
    }),
    {
      // ...
      storage: createJSONStorage(() => AsyncStorage),
    },
  ),
)

partialize

类型:(state: Object) => Object

¥Type: (state: Object) => Object

默认:(state) => state

¥Default: (state) => state

使你能够选择要存储在存储中的一些状态字段。

¥Enables you to pick some of the state's fields to be stored in the storage.

你可以使用以下内容省略多个字段:

¥You could omit multiple fields using the following:

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

或者你可以使用以下内容仅允许特定字段:

¥Or you could allow only specific fields using the following:

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

onRehydrateStorage

类型:(state: Object) => ((state?: Object, error?: Error) => void) | void

¥Type: (state: Object) => ((state?: Object, error?: Error) => void) | void

此选项使你能够传递一个监听器函数,该函数将在存储补充水分时调用。

¥This option enables you to pass a listener function that will be called when the storage is hydrated.

示例:

¥Example:

ts
export const useBoundStore = 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

类型:number

¥Type: number

默认:0

¥Default: 0

如果你想在存储中引入重大更改(例如重命名字段),你可以指定一个新的版本号。默认情况下,如果存储中的版本与代码中的版本不匹配,则不会使用存储的值。你可以使用 migrate 函数(见下文)来处理重大更改,以便持久保存以前存储的数据。

¥If you want to introduce a breaking change in your storage (e.g. renaming a field), you can specify a new version number. By default, if the version in the storage does not match the version in the code, the stored value won't be used. You can use the migrate function (see below) to handle breaking changes in order to persist previously stored data.

migrate

类型:(persistedState: Object, version: number) => Object | Promise<Object>

¥Type: (persistedState: Object, version: number) => Object | Promise<Object>

默认:(persistedState) => persistedState

¥Default: (persistedState) => persistedState

你可以使用此选项来处理版本迁移。迁移函数将持久状态和版本号作为参数。它必须返回符合最新版本(代码中的版本)的状态。

¥You can use this option to handle versions migration. The migrate function takes the persisted state and the version number as arguments. It must return a state that is compliant to the latest version (the version in the code).

例如,如果你想重命名字段,可以使用以下内容:

¥For instance, if you want to rename a field, you can use the following:

ts
export const useBoundStore = 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

类型:(persistedState: Object, currentState: Object) => Object

¥Type: (persistedState: Object, currentState: Object) => Object

默认:(persistedState, currentState) => ({ ...currentState, ...persistedState })

¥Default: (persistedState, currentState) => ({ ...currentState, ...persistedState })

在某些情况下,你可能希望使用自定义合并函数将持久值与当前状态合并。

¥In some cases, you might want to use a custom merge function to merge the persisted value with the current state.

默认情况下,中间件会进行浅合并。如果你有部分持久化的嵌套对象,浅合并可能还不够。例如,如果存储包含以下内容:

¥By default, the middleware does a shallow merge. The shallow merge might not be enough if you have partially persisted nested objects. For instance, if the storage contains the following:

ts
{
  foo: {
    bar: 0,
  }
}

但是你的 Zusand 存储包含:

¥But your Zustand store contains:

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

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

¥The shallow merge will erase the baz field from the foo object. One way to fix this would be to give a custom deep merge function:

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

skipHydration

类型:boolean | undefined

¥Type: boolean | undefined

默认:undefined

¥Default: undefined

默认情况下,存储将在初始化时进行水合。

¥By default the store will be hydrated on initialization.

在某些应用中,你可能需要控制第一次水合发生的时间。例如,在服务器渲染的应用中。

¥In some applications you may need to control when the first hydration occurs. For example, in server-rendered apps.

如果你设置了 skipHydration,则不会调用初始水合调用,你需要手动调用 rehydrate()

¥If you set skipHydration, the initial call for hydration isn't called, and it is left up to you to manually call rehydrate().

ts
export const useBoundStore = create(
  persist(
    () => ({
      count: 0,
      // ...
    }),
    {
      // ...
      skipHydration: true,
    },
  ),
)
tsx
import { useBoundStore } from './path-to-store';

export function StoreConsumer() {
  // hydrate persisted store after on mount
  useEffect(() => {
    useBoundStore.persist.rehydrate();
  }, [])

  return (
    //...
  )
}

API

版本:=3.6.3

¥Version: >=3.6.3

Persist API 使你能够从 React 组件内部或外部与 Persist 中间件进行许多交互。

¥The Persist API enables you to do a number of interactions with the Persist middleware from inside or outside of a React component.

getOptions

类型:() => Partial<PersistOptions>

¥Type: () => Partial<PersistOptions>

返回:Persist 中间件的选项

¥Returns: Options of the Persist middleware

例如,它可用于获取存储名称:

¥For example, it can be used to obtain the storage name:

ts
useBoundStore.persist.getOptions().name

setOptions

类型:(newOptions: Partial<PersistOptions>) => void

¥Type: (newOptions: Partial<PersistOptions>) => void

更改中间件选项。请注意,新选项将与当前选项合并。

¥Changes the middleware options. Note that the new options will be merged with the current ones.

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

¥For instance, this can be used to change the storage name:

ts
useBoundStore.persist.setOptions({
  name: 'new-name',
})

甚至可以更改存储引擎:

¥Or even to change the storage engine:

ts
useBoundStore.persist.setOptions({
  storage: createJSONStorage(() => sessionStorage),
})

clearStorage

类型:() => void

¥Type: () => void

清除 name 键下存储的所有内容。

¥Clears everything stored under the name key.

ts
useBoundStore.persist.clearStorage()

rehydrate

类型:() => Promise<void>

¥Type: () => Promise<void>

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

¥In some cases, you might want to trigger the rehydration manually. This can be done by calling the rehydrate method.

ts
await useBoundStore.persist.rehydrate()

hasHydrated

类型:() => boolean

¥Type: () => boolean

这是一个非反应式 getter,用于检查存储是否已水化(请注意,它在调用 rehydrate 时会更新)。

¥This is a non-reactive getter to check if the storage has been hydrated (note that it updates when calling rehydrate).

ts
useBoundStore.persist.hasHydrated()

onHydrate

类型:(listener: (state) => void) => () => void

¥Type: (listener: (state) => void) => () => void

返回:取消订阅功能

¥Returns: Unsubscribe function

当水合过程开始时,将调用此监听器。

¥This listener will be called when the hydration process starts.

ts
const unsub = useBoundStore.persist.onHydrate((state) => {
  console.log('hydration starts')
})

// later on...
unsub()

onFinishHydration

类型:(listener: (state) => void) => () => void

¥Type: (listener: (state) => void) => () => void

返回:取消订阅功能

¥Returns: Unsubscribe function

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

¥This listener will be called when the hydration process ends.

ts
const unsub = useBoundStore.persist.onFinishHydration((state) => {
  console.log('hydration finished')
})

// later on...
unsub()

createJSONStorage

类型:(getStorage: () => StateStorage, options?: JsonStorageOptions) => StateStorage

¥Type: (getStorage: () => StateStorage, options?: JsonStorageOptions) => StateStorage

返回:PersistStorage

¥Returns: PersistStorage

此辅助函数使你能够创建一个 storage 对象,当你想要使用自定义存储引擎时,该对象很有用。

¥This helper function enables you to create a storage object which is useful when you want to use a custom storage engine.

getStorage 是一个返回具有属性 getItemsetItemremoveItem 的存储引擎的函数。

¥getStorage is a function that returns the storage engine with the properties getItem, setItem, and removeItem.

options 是一个可选对象,可用于自定义数据的序列化和反序列化。options.reviver 是一个传递给 JSON.parse 以反序列化数据的函数。options.replacer 是一个传递给 JSON.stringify 以序列化数据的函数。

¥options is an optional object that can be used to customize the serialization and deserialization of the data. options.reviver is a function that is passed to JSON.parse to deserialize the data. options.replacer is a function that is passed to JSON.stringify to serialize the data.

ts
import { createJSONStorage } from 'zustand/middleware'

const storage = createJSONStorage(() => sessionStorage, {
  reviver: (key, value) => {
    if (value && value.type === 'date') {
      return new Date(value)
    }
    return value
  },
  replacer: (key, value) => {
    if (value instanceof Date) {
      return { type: 'date', value: value.toISOString() }
    }
    return value
  },
})

补水和异步存储

¥Hydration and asynchronous storages

要解释什么是异步存储的 "cost",你需要了解什么是水合。

¥To explain what is the "cost" of asynchronous storages, you need to understand what is hydration.

简而言之,hydration 是从存储中检索持久状态并将其与当前状态合并的过程。

¥In a nutshell, hydration is a process of retrieving persisted state from the storage and merging it with the current state.

Persist 中间件执行两种类型的水合:同步和异步。如果给定的存储是同步的(例如 localStorage),则 hydration 将同步完成。另一方面,如果给定的存储是异步的(例如 AsyncStorage),则水合将异步完成(我知道这很令人震惊!)。

¥The Persist middleware does two kinds of hydration: synchronous and asynchronous. If the given storage is synchronous (e.g., localStorage), hydration will be done synchronously. On the other hand, if the given storage is asynchronous (e.g., AsyncStorage), hydration will be done asynchronously (shocking, I know!).

但有什么问题?通过同步水合,Zustand 存储在创建时就已经水合了。相反,使用异步水合,Zusand 存储将在稍后的微任务中进行水合。

¥But what's the catch? With synchronous hydration, the Zustand store will already have been hydrated at its creation. In contrast, with asynchronous hydration, the Zustand store will be hydrated later on, in a microtask.

为什么这很重要?异步水合可能会导致一些意外行为。例如,如果你在 React 应用中使用 Zusand,则存储将不会在初始渲染时被水化。如果你的应用依赖于页面加载时的持久值,你可能需要等到存储完成水合后再显示任何内容。例如,你的应用可能会认为用户未登录,因为这是默认设置,但实际上存储尚未水化。

¥Why does it matter? Asynchronous hydration can cause some unexpected behaviors. For instance, if you use Zustand in a React app, the store will not be hydrated at the initial render. In cases where your app depends on the persisted value at page load, you might want to wait until the store has been hydrated before showing anything. For example, your app might think the user is not logged in because it's the default, but in reality the store has not been hydrated yet.

如果你的应用确实依赖于页面加载时的持久状态,请参阅下面 常见问题 部分中的 我该如何检查我的存储是否已补水

¥If your app does depends on the persisted state at page load, see How can I check if my store has been hydrated in the FAQ section below.

在 Next.js 中的用法

¥Usage in Next.js

NextJS 使用服务器端渲染,它会将服务器上渲染的组件与客户端上渲染的组件进行比较。但是由于你使用来自浏览器的数据来更改组件,因此两次渲染会有所不同,Next 会向你触发警告。

¥NextJS uses Server Side Rendering, and it will compare the rendered component on the server with the one rendered on client. But since you are using data from browser to change your component, the two renders will differ and Next will throw a warning at you.

错误通常是:

¥The errors usually are:

  • 文本内容与服务器渲染的 HTML 不匹配

    ¥Text content does not match server-rendered HTML

  • Hydration 失败,因为初始 UI 与服务器上渲染的内容不匹配

    ¥Hydration failed because the initial UI does not match what was rendered on the server

  • 水合时出现错误。因为错误发生在 Suspense 边界之外,所以整个根将切换到客户端渲染

    ¥There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering

为了解决这些错误,请创建一个自定义钩子,以便 Zustand 在更改组件之前等待一段时间。

¥To solve these errors, create a custom hook so that Zustand waits a little before changing your components.

创建一个包含以下内容的文件:

¥Create a file with the following:

ts
// useStore.ts
import { useState, useEffect } from 'react'

const useStore = <T, F>(
  store: (callback: (state: T) => unknown) => unknown,
  callback: (state: T) => F,
) => {
  const result = store(callback) as F
  const [data, setData] = useState<F>()

  useEffect(() => {
    setData(result)
  }, [result])

  return data
}

export default useStore

现在在你的页面中,你将以略有不同的方式使用钩子:

¥Now in your pages, you will use the hook a little bit differently:

ts
// useBearStore.ts

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

// the store itself does not need any change
export const useBearStore = create(
  persist(
    (set, get) => ({
      bears: 0,
      addABear: () => set({ bears: get().bears + 1 }),
    }),
    {
      name: 'food-storage',
    },
  ),
)
ts
// yourComponent.tsx

import useStore from './useStore'
import { useBearStore } from './stores/useBearStore'

const bears = useStore(useBearStore, (state) => state.bears)

致谢:对问题的回复,指向 这篇博文

¥Credits: This reply to an issue, which points to this blog post.

常见问题

¥FAQ

我该如何检查我的存储是否已补水

¥How can I check if my store has been hydrated

有几种不同的方法可以做到这一点。

¥There are a few different ways to do this.

你可以使用 onRehydrateStorage 监听器函数更新存储中的字段:

¥You can use the onRehydrateStorage listener function to update a field in the store:

ts
const useBoundStore = create(
  persist(
    (set, get) => ({
      // ...
      _hasHydrated: false,
      setHasHydrated: (state) => {
        set({
          _hasHydrated: state
        });
      }
    }),
    {
      // ...
      onRehydrateStorage: (state) => {
        return () => state.setHasHydrated(true)
      }
    }
  )
);

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

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

  return (
    // ...
  );
}

你还可以创建自定义 useHydration 钩子:

¥You can also create a custom useHydration hook:

ts
const useBoundStore = create(persist(...))

const useHydration = () => {
  const [hydrated, setHydrated] = useState(false)

  useEffect(() => {
    // Note: This is just in case you want to take into account manual rehydration.
    // You can remove the following line if you don't need it.
    const unsubHydrate = useBoundStore.persist.onHydrate(() => setHydrated(false))

    const unsubFinishHydration = useBoundStore.persist.onFinishHydration(() => setHydrated(true))

    setHydrated(useBoundStore.persist.hasHydrated())

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

  return hydrated
}

如何使用自定义存储引擎

¥How can I use a custom storage engine

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

¥If the storage you want to use does not match the expected API, you can create your own storage:

ts
import { create } from 'zustand'
import { persist, createJSONStorage, 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 useBoundStore = create(
  persist(
    (set, get) => ({
      bears: 0,
      addABear: () => set({ bears: get().bears + 1 }),
    }),
    {
      name: 'food-storage', // unique name
      storage: createJSONStorage(() => storage),
    },
  ),
)

如果你使用 JSON.stringify() 不支持的类型,则需要编写自己的序列化/反序列化代码。但是,如果这很繁琐,你可以使用第三方库来序列化和反序列化不同类型的数据。

¥If you're using a type that JSON.stringify() doesn't support, you'll need to write your own serialization/deserialization code. However, if this is tedious, you can use third-party libraries to serialize and deserialize different types of data.

例如,Superjson 可以将数据与其类型一起序列化,从而允许在反序列化时将数据解析回其原始类型

¥For example, Superjson can serialize data along with its type, allowing the data to be parsed back to its original type upon deserialization

ts
import superjson from 'superjson' //  can use anything: serialize-javascript, devalue, etc.
import { PersistStorage } from 'zustand/middleware'

interface BearState {
  bear: Map<string, string>
  fish: Set<string>
  time: Date
  query: RegExp
}

const storage: PersistStorage<BearState> = {
  getItem: (name) => {
    const str = localStorage.getItem(name)
    if (!str) return null
    return superjson.parse(str)
  },
  setItem: (name, value) => {
    localStorage.setItem(name, superjson.stringify(value))
  },
  removeItem: (name) => localStorage.removeItem(name),
}

const initialState: BearState = {
  bear: new Map(),
  fish: new Set(),
  time: new Date(),
  query: new RegExp(''),
}

export const useBearStore = create<BearState>()(
  persist(
    (set) => ({
      ...initialState,
      // ...
    }),
    {
      name: 'food-storage',
      storage,
    },
  ),
)

如何在存储事件中补水

¥How can I rehydrate on storage event

你可以使用 Persist API 创建自己的实现,类似于以下示例:

¥You can use the Persist API to create your own implementation, similar to the example below:

ts
type StoreWithPersist = Mutate<StoreApi<State>, [["zustand/persist", unknown]]>

export const withStorageDOMEvents = (store: StoreWithPersist) => {
  const storageEventCallback = (e: StorageEvent) => {
    if (e.key === store.persist.getOptions().name && e.newValue) {
      store.persist.rehydrate()
    }
  }

  window.addEventListener('storage', storageEventCallback)

  return () => {
    window.removeEventListener('storage', storageEventCallback)
  }
}

const useBoundStore = create(persist(...))
withStorageDOMEvents(useBoundStore)

如何将其与 TypeScript 一起使用

¥How do I use it with TypeScript

除了将 create(...) 写成 create<State>()(...) 之外,基本的 typescript 使用不需要任何特殊操作。

¥Basic typescript usage doesn't require anything special except for writing create<State>()(...) instead of create(...).

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

interface MyState {
  bears: number
  addABear: () => void
}

export const useBearStore = create<MyState>()(
  persist(
    (set, get) => ({
      bears: 0,
      addABear: () => set({ bears: get().bears + 1 }),
    }),
    {
      name: 'food-storage', // name of item in the storage (must be unique)
      storage: createJSONStorage(() => sessionStorage), // (optional) by default the 'localStorage' is used
      partialize: (state) => ({ bears: state.bears }),
    },
  ),
)

如何将其与 Map 和 Set 一起使用

¥How do I use it with Map and Set

为了持久化对象类型(例如 MapSet),需要将它们转换为 JSON 可序列化类型(例如 Array),这可以通过定义自定义 storage 引擎来完成。

¥In order to persist object types such as Map and Set, they will need to be converted to JSON-serializable types such as an Array which can be done by defining a custom storage engine.

假设你的状态使用 Map 来处理 transactions 列表,那么你可以在 storage prop 中将 Map 转换为 Array,如下所示:

¥Let's say your state uses Map to handle a list of transactions, then you can convert the Map into an Array in the storage prop which is shown below:

ts

interface BearState {
  .
  .
  .
  transactions: Map<any>
}

  storage: {
    getItem: (name) => {
      const str = localStorage.getItem(name);
      if (!str) return null;
      const existingValue = JSON.parse(str);
      return {
        ...existingValue,
        state: {
          ...existingValue.state,
          transactions: new Map(existingValue.state.transactions),
        }
      }
    },
    setItem: (name, newValue: StorageValue<BearState>) => {
      // functions cannot be JSON encoded
      const str = JSON.stringify({
        ...newValue,
        state: {
          ...newValue.state,
          transactions: Array.from(newValue.state.transactions.entries()),
        },
      })
      localStorage.setItem(name, str)
    },
    removeItem: (name) => localStorage.removeItem(name),
  },

Zustand v5.0 中文网 - 粤ICP备13048890号