zustand 基于 hooks 的 api,小型、快速且可扩展的状态管理解决方案。
一、安装
yarn add zustand
二、最简使用方法
Step1. 创建一个 Store
store
是一个钩子! 可以在里面放任何东西:原始数据(string,number,bigint,boolean,null)
、对象
、方法
。 set
方法合并状态。
import create from 'zustand'
const useCount = create(set => ({
count: 0,
plus: () => set(state => ({ count: state.count + 1 })),
minus: () => set(state => ({ count: state.count - 1 })),
reset: () => set({ count: 0 })
}))
Step2. 绑定组件
function Counter() {
const count = useCount(state => state.count)
return <h1>{count}</h1>
}
function Controls() {
const plus = useCount(state => state.plus)
const minus= useCount(state => state.minus)
const reset= useCount(state => state.reset)
return (
<>
<button onClick={plus}>+</button>
<button onClick={minus}>-</button>
<button onClick={reset}>重置</button>
</>
)
}
二、强制渲染组件
记住,它会导致组件在每次状态更改时渲染!
const state = useStore()
三、state 更新渲染组件
切片状态(slice state),因为store是一个原子状态,可以将它切分为多个格子状态,便于代码管理。
1. 单个 state 更新渲染
默认情况下,它以严格相等(旧 === 新
)检测更改,这对于原子状态选择非常有效。
const nuts = useStore(state => state.nuts)
const honey = useStore(state => state.honey)
2. 多个 state 更新(浅差异)渲染
如果想构造一个内部有多个 state-picks(状态选择) 的单个对象,类似于 redux 的 mapStateToProps,可以告诉 zustand 你希望通过传递shallow
相等函数来对对象进行浅差异。
|
|
3. 自定义函数控制渲染
为了更好地控制重新渲染,可以提供任何自定义的相等函数。
const treats = useStore(
state => state.treats,
(oldTreats, newTreats) => compare(oldTreats, newTreats)
)
四、记忆处理器(避免重复计算)
1. useCallback() 处理器
通常建议使用 useCallback 记忆处理器。 这将防止每次渲染时进行不必要的计算。 它还允许 React 在并发模式下优化性能。
const fruit = useStore(useCallback(state => state.fruits[id], [id]))
2. 不依赖于作用域的处理器
如果一个处理器不依赖于作用域,可以在渲染函数之外定义它以获得一个固定的“引用”而无需 useCallback。
const selector = state => state.berries
function Component() {
const berries = useStore(selector)
}
五、覆盖state(抹去之前的state)
set
函数有第二个参数,默认为 false。 它将取代state
模型,而不是合并。 注意它会抹去你依赖的部分,比如actions
。
|
|
六、异步操作(fetch请求)
调用 set
, zustand 不关心操作是否异步。
const useStore = create(set => ({
girls: {},
fetch: async pond => {
const response = await fetch(pond)
set({ girls: await response.json() })
}
}))
七、从 action 中读取 state
通过get
访问状态。
const useStore = create((set, get) => ({
name: "Lucy",
action: () => {
const name= get().name
// ...
}
})
八、在 React 组件之外读写 state
1. 自定义 hooks 读写 state
有时需要以非React
方式访问状态,或对store
进行操作。
|
|
2. 使用订阅处理器(中间件)
如果您需要使用处理器订阅,subscribeWithSelector
中间件会有所帮助。
有了这个中间件,subscribe 接受一个额外的签名:
subscribe(selector, callback, options?: { equalityFn, fireImmediately }): Unsubscribe
|
|
3. 订阅处理器(中间件)使用TS
import create, { GetState, SetState } from 'zustand'
import { StoreApiWithSubscribeWithSelector, subscribeWithSelector } from 'zustand/middleware'
type BearState = {
paw: boolean
snout: boolean
fur: boolean
}
const useStore = create<
BearState,
SetState<BearState>,
GetState<BearState>,
StoreApiWithSubscribeWithSelector<BearState>
>(subscribeWithSelector(() => ({ paw: true, snout: true, fur: true })))
九、瞬时更新ref(用于频繁发生的状态变化)
订阅功能允许组件绑定到state,而无需在更改时强制重新渲染。
最好将它与 useEffect
结合使用,以便在卸载时自动取消订阅。
当直接改变视图时,这会对性能产生巨大影响。
const useStore = create(set => ({ girlNum: 0, ... }))
function Component() {
// 获取初始状态
const girlNumRef = useRef(useStore.getState().girlNum)
// 在挂载时连接到Store,在卸载时断开连接,在引用时捕获状态变化
useEffect(() => useStore.subscribe(
state => (girlNumRef.current = state.girlNum)
), [])
十、更新嵌套的状态,使用 Immer
嵌套结构令人厌烦。 可以使用 Immer 处理深层嵌套的state
import produce from 'immer'
const useStore = create(set => ({
lush: { forest: { contains: { a: "bear" } } },
clearForest: () => set(produce(state => {
state.lush.forest.contains = null
}))
}))
const clearForest = useStore(state => state.clearForest)
clearForest();
十一、中间件
1. 按自己喜欢的方式管理store
|
|
2. 管理中间件
|
|
3. 在管理中间件中使用TS
yarn add --dev @types/ramda
|
|
十二、状态持久化中间件 persist
可以存储任何类型的store数据。(localStorage,AsyncStorage,IndexedDB,等…)
1. 快速示例
import create from "zustand"
import { persist } from "zustand/middleware"
export const useStore = create(persist(
(set, get) => ({
count: 0,
plus: () => set({ count: get().count + 1 })
}),
{
name: "count-storage", // 唯一的名称
getStorage: () => sessionStorage, // (可选)默认情况下,使用“localStorage”
}
))
2. 选项
-
name
这是唯一需要的选项。给定的名称
将是用于存储state
的键,因此它必须是唯一
的。 -
getStorage
默认:() => localStorage
给定的存储必须与以下接口匹配:
|
|
serialize
(序列化)
- Schema:
(state: Object) => string | Promise<string>
- Default:
(state) => JSON.stringify(state)
由于将对象存储在 storage 中的唯一方法是通过字符串,可以使用此选项提供自定义函数将 state 序列化为字符串。
例如,如果您想将 state 存储在 base64 中:
export const useStore = create(persist(
(set, get) => ({
// ...
}),
{
// ...
serialize: (state) => btoa(JSON.stringify(state)),
}
))
请注意,还需要一个自定义deserialize
函数才能使其正常工作。见下文
deserialize
(反序列化)
- Schema:
(str: string) => Object | Promise<Object>
- Default:
(str) => JSON.parse(str)
如果使用自定义序列化函数,则很可能还需要使用自定义反序列化函数。它们是一对搭档。
要继续上面的示例,可以使用以下命令反序列化 base64 值:
export const useStore = create(persist(
(set, get) => ({
// ...
}),
{
// ...
deserialize: (str) => JSON.parse(atob(str)),
}
))
partialize
(初始化部分state)
- Schema:
(state: Object) => Object
- Default:
(state) => state
它够省略一些要存储在 storage 中的 state 字段。
可以使用以下方法省略多个字段:
export const useStore = create(persist(
(set, get) => ({
foo: 0,
bar: 1,
}),
{
// ...
partialize: (state) =>
Object.fromEntries(
Object.entries(state).filter(([key]) => !["foo"].includes(key))
),
}
))
或者只允许存储特定state字段:
export const useStore = create(persist(
(set, get) => ({
foo: 0,
bar: 1,
}),
{
// ...
partialize: (state) => ({ foo: state.foo })
}
))
onRehydrateStorage
(水和存储)
- Schema:
(state: Object) => ((state?: Object, error?: Error) => void) | void
此选项能够在水合存储
时调用的侦听器函数。
|
|
version
指定存储版本
- Schema:
number
- Default:
0
如果在 storage 中引入重大更改(例如重命名字段),可以指定新版本号。
默认情况下,如果 storage 中的版本与代码中的版本不匹配,则不会使用 storage 的值。
有关处理重大更改的更多详细信息,请参阅下面的选项migrate
migrate
版本迁移
- Schema:
(persistedState: Object, version: number) => Object | Promise<Object>
- Default:
(persistedState) => persistedState
可以使用此选项来处理版本迁移。migrate 函数将持久化state和版本号作为参数。
它必须返回符合最新版本(代码中的版本)的state。
例如,如果要重命名字段,可以使用以下命令:
|
|
merge
合并存储值和state
-
Schema:
(persistedState: Object, currentState: Object) => Object
-
Default:
(persistedState, currentState) => ({ ...currentState, ...persistedState })
在某些情况下,希望使用自定义合并函数将持久值与当前状态合并。
默认情况下,中间件进行浅合并
。如果部分持久化了嵌套对象,那么浅层合并可能还不够。
例如,如果存储包含以下内容:
{
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),
}
))
3. API
persist api 使 React 组件的内部或外部与持久中间件进行大量交互。
setOptions
更改中间件选项
- Schema:
(newOptions: PersistOptions) => void
此方法能够更改中间件选项。请注意,新选项将与当前选项合并。
例如,这可用于更改 storage 名称:
useStore.persist.setOptions({
name: "new-name"
});
甚至更改 storage 引擎:
useStore.persist.setOptions({
getStorage: () => sessionStorage,
});
clearStorage
- Schema:
() => void
这可用于完全清除 storage 中的持久值。
useStore.persist.clearStorage();
rehydrate
触发水合
- Schema:
() => Promise<void>
在某些情况下,可能希望手动触发水合。可以通过调用该rehydrate
方法来完成。
await useStore.persist.rehydrate();
hasHydrated
获取水合状态
- Schema:
() => boolean
这是一个React的 getter,用于了解存储是否已被水合(请注意,在调用useStore.persist.rehydrate()
时会更新)。
useStore.persist.hasHydrated();
onHydrate
水合过程开始
- Schema:
(listener: (state) => void) => () => void
水合过程开始时将调用给定的侦听器。
|
|
onFinishHydration
水合过程结束
- Schema:
(listener: (state) => void) => () => void
当水化过程结束时,将调用给定的侦听器。
|
|
4. 水合和异步存储
要解释异步存储的“成本”是什么,需要了解什么是水合作用(hydration
)。
1). 什么是水合hydration?
简而言之,水合是从 storage 中检索持久状态并将其与当前状态合并的过程。
persist 中间件执行两种 水合hydration
:同步和异步。
如果给定的存储是同步的(例如localStorage),水合将同步完成,如果给定的存储是异步的(例如AsyncStorage),水合将异步完成……🥁。
但问题是什么?
在同步水合中,Zustand 在创建 store 时进行水合。在异步水合中,Zustand store 将在稍后的微任务中水合。
为什么这有关系?
异步水合可能会导致一些意外行为
(成本所在)。例如,
如果在 React 应用程序中使用 Zustand,store 将不会在初始渲染时水合。
如果应用程序依赖于页面加载时的持久值,你可能希望等到 store 已被水合后再显示内容
(例如,应用程序可能认为用户未登录,因为这是默认值,而实际上 store 还没有被水合)。
2). 如何检查 store 是否已水合
有几种不同的方法可以做到这一点。
fn1. 可以使用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 (
// ...
);
}
fn2. 还可以创建自定义useHydration
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
}
5. 如何使用自定义存储引擎?
如果要使用的存储与预期的 API 不匹配,可以创建自己的存储:
|
|
十三、像 Redux 一样编写代码
const types = { increase: "INCREASE", decrease: "DECREASE" }
const reducer = (state, { type, by = 1 }) => {
switch (type) {
case types.increase: return { grumpiness: state.grumpiness + by }
case types.decrease: return { grumpiness: state.grumpiness - by }
}
}
const useStore = create(set => ({
grumpiness: 0,
dispatch: args => set(state => reducer(state, args)),
}))
const dispatch = useStore(state => state.dispatch)
dispatch({ type: types.increase, by: 2 })
或者,只需使用redux-middleware
。
它连接你的main-reducer
,设置初始 state,并向 state 本身和 vanilla api 添加一个dispatch
函数。
|
|
十四、在 React 事件处理程序之外调用 actions
如果在 React 事件处理程序之外调用setState
,它会同步处理。
在事件处理程序之外更新状态将强制 react 同步更新组件,因此增加了遇到僵尸子效应的风险。
为了解决这个问题,需要将 actions 包裹在unstable_batchedUpdates
中。
import { unstable_batchedUpdates } from 'react-dom' // or 'react-native'
const useStore = create((set) => ({
fishes: 0,
increaseFishes: () => set((prev) => ({ fishes: prev.fishes + 1 }))
}))
const nonReactCallback = () => {
unstable_batchedUpdates(() => {
useStore.getState().increaseFishes()
})
}
十五、使用 Redux 开发工具
|
|
Name store: devtools(store, {name: "MyStore"})
,这将在 devtools 中创建一个名为“MyStore”的单独实例。
序列化选项:devtools(store, { serialize: { options: true } })
。
十六、React context
store create
不需要上下文提供程序(context providers)。
在某些情况下,你可能希望使用上下文进行依赖注入,或者如果你想使用组件中的 props 初始化 store。
因为 store 是一个钩子,把它作为一个普通的上下文值传递可能会违反钩子的规则。
为了避免误用,提供了一个特殊createContext
。
1. 创建 createContext
import create from 'zustand'
import createContext from 'zustand/context'
const { Provider, useStore } = createContext()
const createStore = () => create(...)
const App = () => (
<Provider createStore={createStore}>
...
</Provider>
)
const Component = () => {
const state = useStore()
const slice = useStore(selector)
...
}
2. 在组件中使用
|
|
3. createContext 使用 props 初始化(在 TS 中)
|
|
十七、TypeScript 类型定义
1. 类型定义
|
|
2. 使用combine
并让 tsc 推断类型
这将两个状态浅合并。
import { combine } from 'zustand/middleware'
const useStore = create(
combine(
{ bears: 0 },
(set) => ({ increase: (by: number) => set((state) => ({ bears: state.bears + by })) })
),
)
十八、最佳实践(将store拆分为单独的slice)
目录结构
1.store/createBearSlice.js
const createBearSlice = (set, get) => ({
eatFish: () => {
set((prev) => ({ fishes: prev.fishes > 1 ? prev.fishes - 1 : 0 }));
}
});
export default createBearSlice;
2.store/createFishSlice.js
const maxFishes = 10;
const createFishSlice = (set, get) => ({
fishes: maxFishes,
repopulate: () => {
set((prev) => ({ fishes: maxFishes }));
}
});
export default createFishSlice;
3.store/createHoneySlice.js
const createHoneySlice = (set, get) => ({
honeySlice: {
honey: 4
}
});
export default createHoneySlice;
4.store/useStore.js
import create from "zustand";
import createBearSlice from "./createBearSlice";
import createFishSlice from "./createFishSlice";
import createHoneySlice from "./createHoneySlice";
const useStore = create((set, get) => ({
...createBearSlice(set, get),
...createHoneySlice(set, get),
...createFishSlice(set, get)
}));
export default useStore;
5.pages/Mountain.js
import useStore from "../store/useStore";
export default function Mountain() {
const fishes = useStore((state) => state.fishes);
const eatFish = useStore((state) => state.eatFish);
const repopulate = useStore((state) => state.repopulate);
const honey = useStore((state) => state.honeySlice.honey);
return (
<div className="Mountain">
<p>Fishes : {fishes}</p>
<p>honey : {honey}</p>
<p>
<button onClick={eatFish}>Eat</button>
</p>
<p>
<button onClick={repopulate}>Repopulate</button>
</p>
</div>
);
}