一个小巧、快速且可扩展的基础状态管理解决方案,使用简化的 flux 原则。具有基于 hooks 的舒适 API,不是样板化或有偏见的。
不要因为它可爱就忽视它。它有相当的爪子,花了很多时间处理常见的陷阱,比如可怕的 僵尸子问题、React 并发 和混合渲染器之间的 上下文丢失。它可能是 React 领域中唯一一个能正确处理所有这些问题的状态管理器。
你可以在 这里 试用在线演示。
npm i zustand
你的 store 是一个 hook!你可以在其中放置任何东西:原始值、对象、函数。状态必须以不可变方式更新,并且 set
函数会 合并状态 以帮助实现这一点。
import { create } from 'zustand'
const useBearStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}))
在任何地方使用这个 hook,不需要提供者。选择你的状态,组件将在更改时重新渲染。
function BearCounter() {
const bears = useBearStore((state) => state.bears)
return <h1>{bears} 只熊在这里...</h1>
}
function Controls() {
const increasePopulation = useBearStore((state) => state.increasePopulation)
return <button onClick={increasePopulation}>增加一只</button>
}
- 简单且无偏见
- 使 hooks 成为消费状态的主要手段
- 不会将你的应用包裹在上下文提供者中
- 可以瞬时通知组件(不会导致渲染)
- 更少的样板代码
- 仅在更改时渲染组件
- 集中、基于动作的状态管理
你可以这样做,但请记住,这会导致组件在每次状态更改时更新!
const state = useBearStore()
它默认使用严格相等性(old === new)检测更改,这对于原子状态选择是高效的。
const nuts = useBearStore((state) => state.nuts)
const honey = useBearStore((state) => state.honey)
如果你想构造一个包含多个状态选择的单个对象,类似于 redux 的 mapStateToProps,你可以使用 useShallow 来防止当选择器输出根据浅相等性不变时不必要的重新渲染。
import { create } from 'zustand'
import { useShallow } from 'zustand/react/shallow'
const useBearStore = create((set) => ({
nuts: 0,
honey: 0,
treats: {},
// ...
}))
// 对象选择,当 state.nuts 或 state.honey 更改时重新渲染组件
const { nuts, honey } = useBearStore(
useShallow((state) => ({ nuts: state.nuts, honey: state.honey })),
)
// 数组选择,当 state.nuts 或 state.honey 更改时重新渲染组件
const [nuts, honey] = useBearStore(
useShallow((state) => [state.nuts, state.honey]),
)
// 映射选择,当 state.treats 的顺序、数量或键更改时重新渲染组件
const treats = useBearStore(useShallow((state) => Object.keys(state.treats)))
为了更好地控制重新渲染,你可以提供任何自定义相等函数(此示例需要使用 createWithEqualityFn
)。
const treats = useBearStore(
(state) => state.treats,
(oldTreats, newTreats) => compare(oldTreats, newTreats),
)
set
函数有第二个参数,默认为 false
。它不会合并,而是替换状态模型。小心不要清除你依赖的部分,比如动作。
import omit from 'lodash-es/omit'
const useFishStore = create((set) => ({
salmon: 1,
tuna: 2,
deleteEverything: () => set({}, true), // 清除整个 store,包括动作
deleteTuna: () => set((state) => omit(state, ['tuna']), true),
}))
只需在准备好时调用 set
,zustand 不关心你的动作是否是异步的。
const useFishStore = create((set) => ({
fishies: {},
fetch: async (pond) => {
const response = await fetch(pond)
set({ fishies: await response.json() })
},
}))
set
允许函数更新 set(state => result)
,但你仍然可以通过 get
在其外部访问状态。
const useSoundStore = create((set, get) => ({
sound: 'grunt',
action: () => {
const sound = get().sound
...
有时你需要以非反应方式访问状态或对 store 进行操作。对于这些情况,生成的 hook 在其原型上附加了实用函数。
const useDogStore = create(() => ({ paw: true, snout: true, fur: true }))
// 获取非反应的新鲜状态
const paw = useDogStore.getState().paw
// 监听所有更改,在每次更改时同步触发
const unsub1 = useDogStore.subscribe(console.log)
// 更新状态,将触发监听器
useDogStore.setState({ paw: false })
// 取消订阅监听器
unsub1()
// 你当然可以像往常一样使用 hook
function Component() {
const paw = useDogStore((state) => state.paw)
...
如果你需要使用选择器订阅,
subscribeWithSelector
中间件将有所帮助。
使用此中间件,subscribe
接受一个额外的签名:
subscribe(selector, callback, options?: { equalityFn, fireImmediately }): Unsubscribe
import { subscribeWithSelector } from 'zustand/middleware'
const useDogStore = create(
subscribeWithSelector(() => ({ paw: true, snout: true, fur: true })),
)
// 监听选定的更改,在这种情况下,当 "paw" 更改时
const unsub2 = useDogStore.subscribe((state) => state.paw, console.log)
// 订阅还会暴露先前的值
const unsub3 = useDogStore.subscribe(
(state) => state.paw,
(paw, previousPaw) => console.log(paw, previousPaw),
)
// 订阅还支持可选的相等函数
const unsub4 = useDogStore.subscribe(
(state) => [state.paw, state.fur],
console.log,
{ equalityFn: shallow },
)
// 订阅并立即触发
const unsub5 = useDogStore.subscribe((state) => state.paw, console.log, {
fireImmediately: true,
})
Zustand 核心可以导入并在没有 React 依赖的情况下使用。唯一的区别是 create 函数不返回 hook,而是 API 实用程序。
import { createStore } from 'zustand/vanilla'
const store = createStore((set) => ...)
const { getState, setState, subscribe, getInitialState } = store
export default store
你可以使用 v4 以来可用的 useStore
hook 与 vanilla store 一起使用。
import { useStore } from 'zustand'
import { vanillaStore } from './vanillaStore'
const useBoundStore = (selector) => useStore(vanillaStore, selector)
set
或 get
的中间件不会应用于 getState
和 setState
。
subscribe 函数允许组件绑定到状态部分而不强制在更改时重新渲染。最好将其与 useEffect 结合使用,以便在卸载时自动取消订阅。当你可以直接修改视图时,这可以对性能产生 显著 的影响。
const useScratchStore = create((set) => ({ scratches: 0, ... }))
const Component = () => {
// 获取初始状态
const scratchRef = useRef(useScratchStore.getState().scratches)
// 在挂载时连接到 store,在卸载时断开连接,在引用中捕获状态更改
useEffect(() => useScratchStore.subscribe(
state => (scratchRef.current = state.scratches)
), [])
...
减少嵌套结构是很累人的。你试过 immer 吗?
import { produce } from 'immer'
const useLushStore = create((set) => ({
lush: { forest: { contains: { a: 'bear' } } },
clearForest: () =>
set(
produce((state) => {
state.lush.forest.contains = null
}),
),
}))
const clearForest = useLushStore((state) => state.clearForest)
clearForest()
你可以使用任何类型的存储来持久化你的 store 数据。
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
const useFishStore = create(
persist(
(set, get) => ({
fishes: 0,
addAFish: () => set({ fishes: get().fishes + 1 }),
}),
{
name: 'food-storage', // 存储中的项目名称(必须唯一)
storage: createJSONStorage(() => sessionStorage), // (可选)默认情况下,使用 'localStorage'
},
),
)
Immer 也可以作为中间件使用。
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
const useBeeStore = create(
immer((set) => ({
bees: 0,
addBees: (by) =>
set((state) => {
state.bees += by
}),
})),
)
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 useGrumpyStore = create((set) => ({
grumpiness: 0,
dispatch: (args) => set((state) => reducer(state, args)),
}))
const dispatch = useGrumpyStore((state) => state.dispatch)
dispatch({ type: types.increase, by: 2 })
或者,只需使用我们的 redux 中间件。它会连接你的主 reducer,设置初始状态,并将 dispatch 函数添加到状态本身和 vanilla API。
import { redux } from 'zustand/middleware'
const useGrumpyStore = create(redux(reducer, initialState))
安装 Redux DevTools Chrome 扩展 以使用 devtools 中间件。
import { devtools } from 'zustand/middleware'
// 使用普通动作 store,它将记录动作为 "setState"
const usePlainStore = create(devtools((set) => ...))
// 使用 redux store,它将记录完整的动作类型
const useReduxStore = create(devtools(redux(reducer, initialState)))
一个 redux devtools 连接用于多个 stores
import { devtools } from 'zustand/middleware'
// 使用普通动作 store,它将记录动作为 "setState"
const usePlainStore1 = create(devtools((set) => ..., { name, store: storeName1 }))
const usePlainStore2 = create(devtools((set) => ..., { name, store: storeName2 }))
// 使用 redux store,它将记录完整的动作类型
const useReduxStore = create(devtools(redux(reducer, initialState)), , { name, store: storeName3 })
const useReduxStore = create(devtools(redux(reducer, initialState)), , { name, store: storeName4 })
分配不同的连接名称将分离 stores 在 redux devtools 中。这也有助于将不同的 stores 分组到单独的 redux devtools 连接中。
devtools 将 store 函数作为第一个参数,您可以选择通过第二个参数命名 store 或配置 serialize 选项。
命名 store: devtools(..., {name: "MyStore"})
,这将在 devtools 中创建一个名为 "MyStore" 的单独实例。
序列化选项: devtools(..., { serialize: { options: true } })
。
devtools 只会记录每个分离 store 的动作,不像典型的 combined reducers redux store。请参阅结合 stores 的方法 pmndrs/zustand#163
你可以通过传递第三个参数为每个 set
函数记录特定的动作类型:
const useBearStore = create(devtools((set) => ({
...
eatFish: () => set(
(prev) => ({ fishes: prev.fishes > 1 ? prev.fishes - 1 : 0 }),
undefined,
'bear/eatFish'
),
...
你还可以记录动作的类型及其负载:
...
addFishes: (count) => set(
(prev) => ({ fishes: prev.fishes + count }),
undefined,
{ type: 'bear/addFishes', count, }
),
...
如果未提供动作类型,则默认为 "anonymous"。你可以通过提供 anonymousActionType
参数自定义此默认值:
devtools(..., { anonymousActionType: 'unknown', ... })
如果你希望禁用 devtools(例如在生产环境中)。你可以通过提供 enabled
参数自定义此设置:
devtools(..., { enabled: false, ... })
store 使用 create
创建,不需要上下文提供者。在某些情况下,你可能希望使用上下文进行依赖注入,或者希望使用组件的 props 初始化 store。由于普通 store 是一个 hook,将其作为普通上下文值传递可能会违反 hooks 规则。
自 v4 以来推荐的方法是使用 vanilla store。
import { createContext, useContext } from 'react'
import { createStore, useStore } from 'zustand'
const store = createStore(...) // 没有 hooks 的 vanilla store
const StoreContext = createContext()
const App = () => (
<StoreContext.Provider value={store}>
...
</StoreContext.Provider>
)
const Component = () => {
const store = useContext(StoreContext)
const slice = useStore(store, selector)
...
基本的 TypeScript 使用不需要特别的东西,只需编写 create<State>()(...)
而不是 create(...)
...
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import type {} from '@redux-devtools/extension' // 需要 devtools 类型
interface BearState {
bears: number
increase: (by: number) => void
}
const useBearStore = create<BearState>()(
devtools(
persist(
(set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by })),
}),
{
name: 'bear-storage',
},
),
),
)
更完整的 TypeScript 指南在 这里。
- 你可能想知道如何组织代码以便更好地维护:将 store 拆分为单独的切片。
- 推荐的使用方法: 受 Flux 启发的实践。
- 在 React 18 之前的版本中在 React 事件处理程序外调用动作。
- 测试
- 更多内容,请查看 文档文件夹
一些用户可能希望通过社区制作的第三方库来扩展 Zustand 的功能。有关 Zustand 第三方库的信息,请访问 文档。