• 简体中文
  • 持久化 store 数据

    Persist 中间件让你可以把 Zustand 状态存放进某个存储 (例如 localStorage、AsyncStorage、IndexedDB 等), 从而持久化它的数据。

    注意,该中间件同时支持同步存储 (如 localStorage) 和异步存储 (如 AsyncStorage), 但使用异步存储是有代价的。 更多细节请参阅水合与异步存储。

    简单示例

    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 简单示例

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

    选项

    name

    这是唯一必填的选项。 你提供的 name 会作为在存储中保存 Zustand 状态的 key, 因此它必须唯一。

    storage

    类型:() => StateStorage

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

    import { StateStorage } from 'zustand/middleware'

    默认值:createJSONStorage(() => localStorage)

    让你使用自己的存储。只需传入一个返回所需存储的函数即可。建议使用 createJSONStorage 辅助函数来创建一个符合 StateStorage 接口的 storage 对象。

    示例:

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

    partialize

    类型:(state: Object) => Object

    默认值:(state) => state

    让你挑选状态中的部分字段存入存储。

    你可以用下面的方式排除多个字段:

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

    也可以像下面这样只保留特定字段:

    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

    该选项让你传入一个监听函数, 它会在存储完成水合时被调用。

    示例:

    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

    默认值:0

    如果你想在存储中引入破坏性变更 (例如重命名字段),可以指定一个新的版本号。 默认情况下,如果存储中的版本 与代码中的版本不一致, 已存的值就不会被使用。 你可以使用 migrate 函数(见下文) 处理破坏性变更,从而保住此前存储的数据。

    migrate

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

    默认值:(persistedState) => persistedState

    你可以用该选项处理版本迁移。 migrate 函数接收持久化的状态 和版本号作为参数。 它必须返回一个符合 最新版本(即代码中的版本)的状态。

    例如,如果你想重命名字段,可以这样写:

    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

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

    在某些情况下,你可能想用自定义的 merge 函数 把持久化的值与当前状态合并。

    默认情况下,中间件执行的是浅合并。 如果你只持久化了嵌套对象的一部分, 浅合并可能就不够用了。 例如,存储中包含:

    {
      foo: {
        bar: 0,
      }
    }

    而你的 Zustand store 中包含:

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

    浅合并会把 baz 字段从 foo 对象中抹掉。 一种解决办法是提供一个自定义的深合并函数:

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

    skipHydration

    类型:boolean | undefined

    默认值:undefined

    默认情况下,store 会在初始化时完成水合。

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

    如果设置了 skipHydration,初始的水合调用就不会执行, 你需要自己手动调用 rehydrate()。

    export const useBoundStore = create(
      persist(
        () => ({
          count: 0,
          // ...
        }),
        {
          // ...
          skipHydration: true,
        },
      ),
    )
    import { useBoundStore } from './path-to-store';
    
    export function StoreConsumer() {
      // hydrate persisted store after on mount
      useEffect(() => {
        useBoundStore.persist.rehydrate();
      }, [])
    
      return (
        //...
      )
    }

    API

    版本:>=3.6.3

    Persist API 让你可以在 React 组件内部或外部 与 Persist 中间件进行多种交互。

    getOptions

    类型:() => Partial<PersistOptions>

    返回值:Persist 中间件的选项

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

    useBoundStore.persist.getOptions().name

    setOptions

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

    修改中间件的选项。 注意,新选项会与当前选项合并。

    例如,可以用它更改存储的名称:

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

    甚至可以更改存储引擎:

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

    clearStorage

    类型:() => void

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

    useBoundStore.persist.clearStorage()

    rehydrate

    类型:() => Promise<void>

    在某些情况下,你可能想手动触发重新水合(rehydrate)。 调用 rehydrate 方法即可。

    await useBoundStore.persist.rehydrate()

    hasHydrated

    类型:() => boolean

    这是一个非响应式的 getter,用于检查 存储是否已经完成水合 (注意,调用 rehydrate 时它会更新)。

    useBoundStore.persist.hasHydrated()

    onHydrate

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

    返回值:取消订阅函数

    该监听函数会在水合过程开始时被调用。

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

    onFinishHydration

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

    返回值:取消订阅函数

    该监听函数会在水合过程结束时被调用。

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

    createJSONStorage

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

    返回值:PersistStorage

    该辅助函数让你创建一个 storage 对象,在你需要使用自定义存储引擎时很有用。

    getStorage 是一个函数,返回带有 getItem、setItem 和 removeItem 属性的存储引擎。

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

    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) => {
        // NOTE: the result of `.toJSON()` is passed to the
        // replacer function as value if is available so
        // a Date is always a `string` at this point
        if (key === 'someDate') return { type: 'date', value }
        return value
      },
    })

    水合与异步存储

    要解释异步存储的“代价”是什么, 你需要先理解什么是水合。

    简而言之,水合是一个 从存储中取出持久化状态 并将其与当前状态合并的过程。

    Persist 中间件会做两种水合: 同步水合和异步水合。 如果给定的存储是同步的(如 localStorage), 水合就会同步完成。 反之,如果给定的存储是异步的(如 AsyncStorage), 水合就会异步完成(很意外吧,我知道!)。

    但问题在哪呢? 使用同步水合时, Zustand store 在创建时就已经完成水合了。 相反,使用异步水合时, Zustand store 会在之后的一个微任务中才完成水合。

    为什么这很重要? 异步水合可能引发一些意料之外的行为。 例如,如果你在 React 应用中使用 Zustand, store 在首次渲染时不会完成水合。 如果你的应用依赖页面加载时的持久化值, 你可能需要等到 store 完成水合之后再渲染任何内容。 例如,你的应用可能因为默认值而认为用户 尚未登录, 但实际上 store 还没有完成水合。

    如果你的应用确实依赖页面加载时的持久化状态, 请参阅下文 FAQ 一节中的 如何检查我的 store 是否已完成水合。

    在 Next.js 中使用

    Next.js 使用服务端渲染,它会比较服务端渲染的组件与客户端渲染的组件。 但由于你在用浏览器的数据来改变组件,两次渲染的结果会不同,Next 就会向你抛出一个警告。

    报错通常是:

    • Text content does not match server-rendered HTML
    • Hydration failed because the initial UI does not match what was rendered on the server
    • There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering

    要解决这些报错,可以创建一个自定义 Hook,让 Zustand 稍等片刻再修改你的组件。

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

    // 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

    现在在页面中,你需要稍微不同地使用这个 Hook:

    // 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',
        },
      ),
    )
    // yourComponent.tsx
    
    import useStore from './useStore'
    import { useBearStore } from './stores/useBearStore'
    
    const bears = useStore(useBearStore, (state) => state.bears)

    致谢:这条 issue 回复,它指向了这篇博客文章。

    FAQ

    如何检查我的 store 是否已完成水合

    有几种不同的做法。

    你可以使用 onRehydrateStorage 监听函数来更新 store 中的某个字段:

    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 Hook:

    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
    }

    如何使用自定义存储引擎

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

    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() 支持,就需要自己编写序列化/反序列化代码。不过,如果觉得这太繁琐,也可以用第三方库来序列化和反序列化不同类型的数据。

    例如,Superjson 可以在序列化数据时一并保存其类型,从而在反序列化时把数据解析回原始类型

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

    如何在 storage 事件触发时重新水合

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

    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 中使用

    基本的 TypeScript 用法不需要任何特殊处理, 只需写成 create<State>()(...) 而不是 create(...)。

    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 中使用

    为了持久化 Map 和 Set 这类对象类型,需要把它们转换成可 JSON 序列化的类型(例如 Array),这可以通过定义自定义的 storage 引擎来实现。

    假设你的状态用 Map 来处理一组 transactions, 那么你可以在 storage 属性中把 Map 转换成 Array,如下所示:

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