persist
persist 中间件让你可以在页面刷新或应用重启后保留 store 的状态。
const nextStateCreatorFn = persist(stateCreatorFn, persistOptions)
类型
签名
persist<T, U = T>(stateCreatorFn: StateCreator<T, [], []>, persistOptions: PersistOptions<T, U>): StateCreator<T, [['zustand/persist', U]], []>
Mutator
参考
persist(stateCreatorFn, persistOptions)
参数
stateCreatorFn:一个接收 set 函数、get 函数和 store 作为参数的函数。通常,你会返回一个对象,其中包含你想要暴露的方法。
persistOptions:一个用于定义存储选项的对象。
name:store 在存储中对应条目的唯一名称。
- 可选
storage:一个用于读写持久化状态的 PersistStorage 对象。默认为 createJSONStorage(() => localStorage)。传给 createJSONStorage 的 getter 函数是惰性求值的(只在首次访问存储时才执行),这可以避免在模块求值阶段无法使用 localStorage 这类存储 API 的环境中出错——例如服务端渲染(SSR)期间。
警告: createJSONStorage 依赖 JSON.parse 和 JSON.stringify,不执行任何运行时校验。从存储中读取的值会被直接断言为你的状态类型,而不会检查其结构,因此损坏、过期或被篡改的数据在运行时不会被发现。生产环境中,请实现自定义的 PersistStorage 来校验反序列化后的值——例如使用 Zod 这类 schema 校验库。
- 可选
partialize:一个用于在持久化之前过滤状态字段的函数。
- 可选
onRehydrateStorage:一个函数,或一个返回函数的函数,让你可以在状态重新水合(rehydrate)前后执行自定义逻辑。
- 可选
version:持久化状态的版本号。如果存储中的状态版本不匹配,就不会使用它。
- 可选
migrate:当版本不匹配时,用于迁移持久化状态的函数。
- 可选
merge:在重新水合期间把持久化状态与当前状态合并时,用于自定义逻辑的函数。默认为浅合并。
- 可选
skipHydration:默认为 false。如果为 true,中间件不会在初始化时自动重新水合状态。这种情况下需要手动调用 rehydrate 函数。这适用于服务端渲染(SSR)应用。
返回值
persist 返回一个状态创建函数。
用法
持久化状态
在本教程中,我们将使用原生 store 和 persist 中间件创建一个简单的位置追踪器。这个示例会追踪鼠标在容器内移动时的 position,并把 position 存入本地存储,这样即使页面刷新,位置也能保留下来。
首先,我们创建一个原生 store,用来保存位置(一个包含 x 和 y 坐标的对象)以及更新它的 action。我们还会使用 persist 中间件把位置存入 localStorage。
import { createStore } from 'zustand/vanilla'
import { persist } from 'zustand/middleware'
type PositionStoreState = { position: { x: number; y: number } }
type PositionStoreActions = {
setPosition: (nextPosition: PositionStoreState['position']) => void
}
type PositionStore = PositionStoreState & PositionStoreActions
const positionStore = createStore<PositionStore>()(
persist(
(set) => ({
position: { x: 0, y: 0 },
setPosition: (position) => set({ position }),
}),
{ name: 'position-storage' },
),
)
接下来,我们会在一个 div 内追踪鼠标移动,并用新的位置更新 store。
const $dotContainer = document.getElementById('dot-container') as HTMLDivElement
const $dot = document.getElementById('dot') as HTMLDivElement
$dotContainer.addEventListener('pointermove', (event) => {
positionStore.getState().setPosition({
x: event.clientX,
y: event.clientY,
})
})
我们希望通过把一个 div 元素(代表小圆点)移动到新坐标,让位置的更新反映在屏幕上。
const render: Parameters<typeof positionStore.subscribe>[0] = (state) => {
$dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)`
}
render(positionStore.getState(), positionStore.getState())
positionStore.subscribe(render)
以下是完整代码。
import { createStore } from 'zustand/vanilla'
import { persist } from 'zustand/middleware'
type PositionStoreState = { position: { x: number; y: number } }
type PositionStoreActions = {
setPosition: (nextPosition: PositionStoreState['position']) => void
}
type PositionStore = PositionStoreState & PositionStoreActions
const positionStore = createStore<PositionStore>()(
persist(
(set) => ({
position: { x: 0, y: 0 },
setPosition: (position) => set({ position }),
}),
{ name: 'position-storage' },
),
)
const $dotContainer = document.getElementById('dot-container') as HTMLDivElement
const $dot = document.getElementById('dot') as HTMLDivElement
$dotContainer.addEventListener('pointermove', (event) => {
positionStore.getState().setPosition({
x: event.clientX,
y: event.clientY,
})
})
const render: Parameters<typeof positionStore.subscribe>[0] = (state) => {
$dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)`
}
render(positionStore.getState(), positionStore.getState())
positionStore.subscribe(render)
以下是 html 代码
<div id="dot-container" style="position: relative; width: 100vw; height: 100vh;">
<div
id="dot"
style="position: absolute; background-color: red; border-radius: 50%; left: -10px; top: -10px; width: 20px; height: 20px;"
></div>
</div>
部分持久化状态
在本教程中,我们将使用原生 store 和 persist 中间件创建一个简单的位置追踪器。此外,我们还会展示如何只持久化状态的一部分(部分持久化),当你不想把整个状态都存入 localStorage 时,这会很有用。
我们首先创建一个原生 store,用来保存位置状态以及更新它的 action。我们将使用 persist 中间件只持久化状态中相关的部分(本例中是包含位置信息的 context)。
import { createStore } from 'zustand/vanilla'
import { persist } from 'zustand/middleware'
type PositionStoreState = {
context: {
position: { x: number; y: number }
}
}
type PositionStoreActions = {
actions: {
setPosition: (nextPosition: PositionStoreState['context']['position']) => void
}
}
type PositionStore = PositionStoreState & PositionStoreActions
const positionStore = createStore<PositionStore>()(
persist(
(set) => ({
context: {
position: { x: 0, y: 0 },
},
actions: {
setPosition: (position) => set({ context: { position } }),
},
}),
{
name: 'position-storage',
partialize: (state) => ({ context: state.context }),
},
),
)
接下来,我们会在一个 div 内追踪鼠标移动,并用新的位置更新 store。
const $dotContainer = document.getElementById('dot-container') as HTMLDivElement
const $dot = document.getElementById('dot') as HTMLDivElement
$dotContainer.addEventListener('pointermove', (event) => {
positionStore.getState().actions.setPosition({
x: event.clientX,
y: event.clientY,
})
})
我们希望通过把一个 div 元素(代表小圆点)移动到新坐标,让位置的更新反映在屏幕上。
const render: Parameters<typeof positionStore.subscribe>[0] = (state) => {
$dot.style.transform = `translate(${state.context.position.x}px, ${state.context.position.y}px)`
}
render(positionStore.getState(), positionStore.getState())
positionStore.subscribe(render)
以下是创建一个小圆点、让它跟随你在容器内的鼠标移动,并把 context 持久化到 localStorage 的完整代码。
import { createStore } from 'zustand/vanilla'
import { persist } from 'zustand/middleware'
type PositionStoreState = {
context: {
position: { x: number; y: number }
}
}
type PositionStoreActions = {
actions: {
setPosition: (nextPosition: PositionStoreState['context']['position']) => void
}
}
type PositionStore = PositionStoreState & PositionStoreActions
const positionStore = createStore<PositionStore>()(
persist(
(set) => ({
context: {
position: { x: 0, y: 0 },
},
actions: {
setPosition: (position) => set({ context: { position } }),
},
}),
{
name: 'position-storage',
partialize: (state) => ({ context: state.context }),
},
),
)
const $dotContainer = document.getElementById('dot-container') as HTMLDivElement
const $dot = document.getElementById('dot') as HTMLDivElement
$dotContainer.addEventListener('pointermove', (event) => {
positionStore.getState().actions.setPosition({
x: event.clientX,
y: event.clientY,
})
})
const render: Parameters<typeof positionStore.subscribe>[0] = (state) => {
$dot.style.transform = `translate(${state.context.position.x}px, ${state.context.position.y}px)`
}
render(positionStore.getState(), positionStore.getState())
positionStore.subscribe(render)
以下是 html 代码
<div id="dot-container" style="position: relative; width: 100vw; height: 100vh;">
<div
id="dot"
style="position: absolute; background-color: red; border-radius: 50%; left: -10px; top: -10px; width: 20px; height: 20px;"
></div>
</div>
使用自定义存储持久化状态
注意: createJSONStorage 是一个适合快速原型的便捷辅助函数。它使用 JSON.parse/JSON.stringify 而不做运行时校验,因此存储中意外或损坏的数据在运行时不会被发现。生产应用中,请实现自定义的 PersistStorage 来校验持久化的值,例如使用 Zod 这类 schema 校验库。
在这个小教程中,我们将使用原生 store 创建一个简单的位置追踪系统,把位置状态持久化到 URL 的搜索参数中。这种方式让状态可以直接持久化在浏览器的 URL 里,对于在页面刷新后保持状态或分享内嵌了状态的链接都很有用。
我们需要实现一些函数,把 URL 搜索参数当作存储机制来操作,包括读取、设置和删除参数。
const getSearchParams = () => {
return new URL(location.href).searchParams
}
const updateSearchParams = (searchParams: URLSearchParams) => {
window.history.replaceState({}, '', `${location.pathname}?${searchParams.toString()}`)
}
const getSearchParam = (key: string) => {
const searchParams = getSearchParams()
return searchParams.get(key)
}
const updateSearchParam = (key: string, value: string) => {
const searchParams = getSearchParams()
searchParams.set(key, value)
updateSearchParams(searchParams)
}
const removeSearchParam = (key: string) => {
const searchParams = getSearchParams()
searchParams.delete(key)
updateSearchParams(searchParams)
}
为了把 URL 搜索参数当作存储使用,我们定义一个包含 getItem、setItem 和 removeItem 方法的 searchParamsStorage 对象。这些方法映射到我们操作搜索参数的自定义函数。
const searchParamsStorage = {
getItem: (key: string) => getSearchParam(key),
setItem: (key: string, value: string) => updateSearchParam(key, value),
removeItem: (key: string) => removeSearchParam(key),
}
现在,我们使用 persist 中间件初始化原生 store,并指定要使用自定义存储。我们不再使用默认的 localStorage 或 sessionStorage,而是把位置数据持久化到 URL 搜索参数中。
import { createStore } from 'zustand/vanilla'
import { persist, createJSONStorage } from 'zustand/middleware'
type PositionStoreState = { position: { x: number; y: number } }
type PositionStoreActions = {
setPosition: (nextPosition: PositionStoreState['position']) => void
}
type PositionStore = PositionStoreState & PositionStoreActions
const positionStore = createStore<PositionStore>()(
persist(
(set) => ({
position: { x: 0, y: 0 },
setPosition: (position) => set({ position }),
}),
{
name: 'position-storage',
storage: createJSONStorage(() => searchParamsStorage),
},
),
)
接下来,我们会在一个 div 内追踪鼠标移动,并用新的位置更新 store。
const $dotContainer = document.getElementById('dot-container') as HTMLDivElement
const $dot = document.getElementById('dot') as HTMLDivElement
$dotContainer.addEventListener('pointermove', (event) => {
positionStore.getState().setPosition({
x: event.clientX,
y: event.clientY,
})
})
我们希望通过把一个 div 元素(代表小圆点)移动到新坐标,让位置的更新反映在屏幕上。
const render: Parameters<typeof positionStore.subscribe>[0] = (state) => {
$dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)`
}
render(positionStore.getState(), positionStore.getState())
positionStore.subscribe(render)
以下是创建一个小圆点、让它跟随你在容器内的鼠标移动,并把位置持久化到 URL 搜索参数中的完整代码。
import { createStore } from 'zustand/vanilla'
import { persist, createJSONStorage } from 'zustand/middleware'
type PositionStoreState = { position: { x: number; y: number } }
type PositionStoreActions = {
setPosition: (nextPosition: PositionStoreState['position']) => void
}
type PositionStore = PositionStoreState & PositionStoreActions
const getSearchParams = () => {
return new URL(location.href).searchParams
}
const updateSearchParams = (searchParams: URLSearchParams) => {
window.history.replaceState({}, '', `${location.pathname}?${searchParams.toString()}`)
}
const getSearchParam = (key: string) => {
const searchParams = getSearchParams()
return searchParams.get(key)
}
const updateSearchParam = (key: string, value: string) => {
const searchParams = getSearchParams()
searchParams.set(key, value)
updateSearchParams(searchParams)
}
const removeSearchParam = (key: string) => {
const searchParams = getSearchParams()
searchParams.delete(key)
updateSearchParams(searchParams)
}
const searchParamsStorage = {
getItem: (key: string) => getSearchParam(key),
setItem: (key: string, value: string) => updateSearchParam(key, value),
removeItem: (key: string) => removeSearchParam(key),
}
const positionStore = createStore<PositionStore>()(
persist(
(set) => ({
position: { x: 0, y: 0 },
setPosition: (position) => set({ position }),
}),
{
name: 'position-storage',
storage: createJSONStorage(() => searchParamsStorage),
},
),
)
const $dotContainer = document.getElementById('dot-container') as HTMLDivElement
const $dot = document.getElementById('dot') as HTMLDivElement
$dotContainer.addEventListener('pointermove', (event) => {
positionStore.getState().setPosition({
x: event.clientX,
y: event.clientY,
})
})
const render: Parameters<typeof positionStore.subscribe>[0] = (state) => {
$dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)`
}
render(positionStore.getState(), positionStore.getState())
positionStore.subscribe(render)
以下是 html 代码
<div id="dot-container" style="position: relative; width: 100vw; height: 100vh;">
<div
id="dot"
style="position: absolute; background-color: red; border-radius: 50%; left: -10px; top: -10px; width: 20px; height: 20px;"
></div>
</div>
通过版本管理与迁移持久化状态
在本教程中,我们将探讨如何使用版本管理与迁移来管理状态持久化。我们会演示如何在不破坏已有持久化数据的前提下,让状态 schema 跨版本演进。
在进入带版本的状态管理之前,我们先模拟一个 version 0 的初始状态:如果该状态在 localStorage 中还不存在,就手动写入一个 version 0 的状态。version 0 的状态把坐标保存为 x 和 y 字段。
// For tutorial purposes only
if (!localStorage.getItem('position-storage')) {
localStorage.setItem(
'position-storage',
JSON.stringify({
state: { x: 100, y: 100 }, // version 0 structure
version: 0,
}),
)
}
接下来,我们使用 persist 中间件处理状态持久化。我们还会添加一个迁移函数来处理版本之间的变化。在这个示例中,我们通过 migrate 把状态从 version 0(x 和 y 是分开的)迁移到 version 1(二者合并为 position 对象)。
migrate: (persisted: any, version) => {
if (version === 0) {
persisted.position = { x: persisted.x, y: persisted.y }
delete persisted.x
delete persisted.y
}
return persisted
}
接下来,我们会在一个 div 内追踪鼠标移动,并用新的位置更新 store。
const $dotContainer = document.getElementById('dot-container') as HTMLDivElement
const $dot = document.getElementById('dot') as HTMLDivElement
$dotContainer.addEventListener('pointermove', (event) => {
positionStore.getState().setPosition({
x: event.clientX,
y: event.clientY,
})
})
我们希望通过把一个 div 元素(代表小圆点)移动到新坐标,让位置的更新反映在屏幕上。
const render: Parameters<typeof positionStore.subscribe>[0] = (state) => {
$dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)`
}
render(positionStore.getState(), positionStore.getState())
positionStore.subscribe(render)
以下是完整代码。
import { createStore } from 'zustand/vanilla'
import { persist } from 'zustand/middleware'
// For tutorial purposes only
if (!localStorage.getItem('position-storage')) {
localStorage.setItem(
'position-storage',
JSON.stringify({
state: { x: 100, y: 100 },
version: 0,
}),
)
}
type PositionStoreState = { position: { x: number; y: number } }
type PositionStoreActions = {
setPosition: (nextPosition: PositionStoreState['position']) => void
}
type PositionStore = PositionStoreState & PositionStoreActions
const positionStore = createStore<PositionStore>()(
persist(
(set) => ({
position: { x: 0, y: 0 }, // version 0: just x: 0, y: 0
setPosition: (position) => set({ position }),
}),
{
name: 'position-storage',
version: 1,
migrate: (persisted: any, version) => {
if (version === 0) {
persisted.position = { x: persisted.x, y: persisted.y }
delete persisted.x
delete persisted.y
}
return persisted
},
},
),
)
const $dotContainer = document.getElementById('dot-container') as HTMLDivElement
const $dot = document.getElementById('dot') as HTMLDivElement
$dotContainer.addEventListener('pointermove', (event) => {
positionStore.getState().setPosition({
x: event.clientX,
y: event.clientY,
})
})
const render: Parameters<typeof positionStore.subscribe>[0] = (state) => {
$dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)`
}
render(positionStore.getState(), positionStore.getState())
positionStore.subscribe(render)
以下是 html 代码
<div id="dot-container" style="position: relative; width: 100vw; height: 100vh;">
<div
id="dot"
style="position: absolute; background-color: red; border-radius: 50%; left: -10px; top: -10px; width: 20px; height: 20px;"
></div>
</div>
持久化包含嵌套对象的状态
在本教程中,我们将创建一个原生 store,用来追踪由 x 和 y 坐标表示的位置。我们还会使用 localStorage 实现持久化,并演示在状态可能缺少字段时如何合并。
为了模拟教程的初始状态,我们会检查位置数据是否已存在于 localStorage 中。如果不存在,就把它设置好。
if (!localStorage.getItem('position-storage')) {
localStorage.setItem(
'position-storage',
JSON.stringify({
state: { position: { y: 100 } }, // missing `x` field
version: 0,
}),
)
}
现在,我们创建 store,并把它配置为使用持久化和深度合并。
import { createStore } from 'zustand/vanilla'
import { persist } from 'zustand/middleware'
import createDeepMerge from '@fastify/deepmerge'
const deepMerge = createDeepMerge({ all: true })
type PositionStoreState = { position: { x: number; y: number } }
type PositionStoreActions = {
setPosition: (nextPosition: PositionStoreState['position']) => void
}
type PositionStore = PositionStoreState & PositionStoreActions
const positionStore = createStore<PositionStore>()(
persist(
(set) => ({
position: { x: 0, y: 0 },
setPosition: (position) => set({ position }),
}),
{
name: 'position-storage',
merge: (persisted, current) => deepMerge(current, persisted) as never,
},
),
)
接下来,我们会在一个 div 内追踪鼠标移动,并用新的位置更新 store。
const $dotContainer = document.getElementById('dot-container') as HTMLDivElement
const $dot = document.getElementById('dot') as HTMLDivElement
$dotContainer.addEventListener('pointermove', (event) => {
positionStore.getState().setPosition({
x: event.clientX,
y: event.clientY,
})
})
我们希望通过把一个 div 元素(代表小圆点)移动到新坐标,让位置的更新反映在屏幕上。
const render: Parameters<typeof positionStore.subscribe>[0] = (state) => {
$dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)`
}
render(positionStore.getState(), positionStore.getState())
positionStore.subscribe(render)
以下是完整代码。
import { createStore } from 'zustand/vanilla'
import { persist } from 'zustand/middleware'
import createDeepMerge from '@fastify/deepmerge'
const deepMerge = createDeepMerge({ all: true })
// For tutorial purposes only
if (!localStorage.getItem('position-storage')) {
localStorage.setItem(
'position-storage',
JSON.stringify({
state: { position: { y: 100 } }, // missing `x` field
version: 0,
}),
)
}
type PositionStoreState = { position: { x: number; y: number } }
type PositionStoreActions = {
setPosition: (nextPosition: PositionStoreState['position']) => void
}
type PositionStore = PositionStoreState & PositionStoreActions
const positionStore = createStore<PositionStore>()(
persist(
(set) => ({
position: { x: 0, y: 0 },
setPosition: (position) => set({ position }),
}),
{
name: 'position-storage',
merge: (persisted, current) => deepMerge(current, persisted) as never,
},
),
)
const $dotContainer = document.getElementById('dot-container') as HTMLDivElement
const $dot = document.getElementById('dot') as HTMLDivElement
$dotContainer.addEventListener('pointermove', (event) => {
positionStore.getState().setPosition({
x: event.clientX,
y: event.clientY,
})
})
const render: Parameters<typeof positionStore.subscribe>[0] = (state) => {
console.log({ state })
$dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)`
}
render(positionStore.getState(), positionStore.getState())
positionStore.subscribe(render)
以下是 html 代码
<div id="dot-container" style="position: relative; width: 100vw; height: 100vh;">
<div
id="dot"
style="position: absolute; background-color: red; border-radius: 50%; left: -10px; top: -10px; width: 20px; height: 20px;"
></div>
</div>
持久化状态并手动水合
在本教程中,我们将创建一个原生 store,用来追踪由 x 和 y 坐标表示的位置。我们还会使用 localStorage 实现持久化,并探讨如何跳过水合过程,在延迟之后手动触发重新水合。
我们首先创建一个原生 store,用来保存位置(一个包含 x 和 y 坐标的对象)以及更新它的 action。此外,我们还会使用 persist 中间件把位置存入 localStorage,但跳过水合。
import { createStore } from 'zustand/vanilla'
import { persist } from 'zustand/middleware'
type PositionStoreState = { position: { x: number; y: number } }
type PositionStoreActions = {
setPosition: (nextPosition: PositionStoreState['position']) => void
}
type PositionStore = PositionStoreState & PositionStoreActions
const positionStore = createStore<PositionStore>()(
persist(
(set) => ({
position: { x: 0, y: 0 },
setPosition: (position) => set({ position }),
}),
{
name: 'position-storage',
skipHydration: true,
},
),
)
由于我们在初始设置中跳过了水合,我们将手动重新水合状态。这里我们使用 setTimeout 来模拟延迟重新水合。
setTimeout(() => {
positionStore.persist.rehydrate()
}, 2000)
接下来,我们会在一个 div 内追踪鼠标移动,并用新的位置更新 store。
const $dotContainer = document.getElementById('dot-container') as HTMLDivElement
const $dot = document.getElementById('dot') as HTMLDivElement
$dotContainer.addEventListener('pointermove', (event) => {
positionStore.getState().setPosition({
x: event.clientX,
y: event.clientY,
})
})
我们希望通过把一个 div 元素(代表小圆点)移动到新坐标,让位置的更新反映在屏幕上。
const render: Parameters<typeof positionStore.subscribe>[0] = (state) => {
$dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)`
}
render(positionStore.getState(), positionStore.getState())
positionStore.subscribe(render)
以下是完整代码。
import { createStore } from 'zustand/vanilla'
import { persist } from 'zustand/middleware'
type PositionStoreState = { position: { x: number; y: number } }
type PositionStoreActions = {
setPosition: (nextPosition: PositionStoreState['position']) => void
}
type PositionStore = PositionStoreState & PositionStoreActions
const positionStore = createStore<PositionStore>()(
persist(
(set) => ({
position: { x: 0, y: 0 },
setPosition: (position) => set({ position }),
}),
{
name: 'position-storage',
skipHydration: true,
},
),
)
const $dotContainer = document.getElementById('dot-container') as HTMLDivElement
const $dot = document.getElementById('dot') as HTMLDivElement
$dotContainer.addEventListener('pointermove', (event) => {
positionStore.getState().setPosition({
x: event.clientX,
y: event.clientY,
})
})
const render: Parameters<typeof positionStore.subscribe>[0] = (state) => {
$dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)`
}
setTimeout(() => {
positionStore.persist.rehydrate()
}, 2000)
render(positionStore.getState(), positionStore.getState())
positionStore.subscribe(render)
以下是 html 代码
<div id="dot-container" style="position: relative; width: 100vw; height: 100vh;">
<div
id="dot"
style="position: absolute; background-color: red; border-radius: 50%; left: -10px; top: -10px; width: 20px; height: 20px;"
></div>
</div>
疑难解答
TBD