主题
保持存储数据
¥Persisting store data
Persist 中间件使你能够将 Zustand 状态存储在存储中(例如 localStorage
、AsyncStorage
、IndexedDB
等),从而持久保存其数据。
¥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
是一个返回具有属性 getItem
、setItem
和 removeItem
的存储引擎的函数。
¥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
为了持久化对象类型(例如 Map
和 Set
),需要将它们转换为 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),
},