一个小巧、快速且可扩展的精简状态管理解决方案,使用简化的 Flux 原则。它有一个基于 hooks 的简洁 API,不会产生样板代码或具有强烈的观点。
不要因为它很可爱而忽视它。它具有相当强大的功能,花费了很多时间来处理常见陷阱,比如可怕的僵尸子组件问题、React 并发和混合渲染器之间的上下文丢失。它可能是 React 领域中唯一能解决所有这些问题的状态管理器。
bash
npm install zustand #或yarn添加zustand或PNPM添加zustand
:warning:这个自述文件是为JavaScript用户编写的。如果你是TypeScript用户,一定要查看我们的TypeScript用法部分。
首先创建一个存储
您的存储是一个钩子(hook)!您可以在其中存储任何内容:原始值、对象、函数。状态必须以不可变的方式进行更新,并且 set
函数会合并状态以帮助进行更新。
jsx
import { create } from 'zustand'
const useBearStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}))
然后将您的组件绑定到存储中,就完成了!
您可以在任何地方使用这个钩子,无需提供者(providers)。选择您的状态,并且当状态发生变化时,组件将重新渲染。
jsx
function BearCounter() {
const bears = useBearStore((state) => state.bears)
return <h1>{bears} around here ...</h1>
}
function Controls() {
const increasePopulation = useBearStore((state) => state.increasePopulation)
return <button onClick={increasePopulation}>one up</button>
}
相对于 Redux,为什么选择使用 Zustand?
- 简单和不附加特定偏好:Zustand 是一个简单且不附加特定偏好的状态管理库。它允许您以自己喜欢的方式组织和处理状态,而不会强加特定的架构或模式。
- 将 hooks 作为主要状态消费方式:Zustand 将 hooks(钩子)作为主要方式来消费状态。这意味着您可以在函数组件中直接使用 useState、useEffect 等 React 钩子函数,并且通过调用 Zustand 中提供的 useSelector、useStore 等钩子来访问和更新状态。
- 不包装整个应用程序:相较于 Redux,Zustand 不需要将整个应用程序包装在上下文提供者中。它只需创建一个状态存储,并通过 hooks 将其连接到需要访问状态的组件中。这简化了代码结构和组件层次结构,并减少了上下文的嵌套。
- 可以以暂时的方式通知组件(无需引发渲染):Zustand 提供了一种可以以暂时的方式通知组件的机制,而无需引发重新渲染。通过使用 Zustand 的订阅机制,您可以在状态更改时触发副作用,而不会导致与状态无关的组件重新渲染。
为什么选择 Zustand 而不是 React 的 Context?
- 更少的样板代码:与直接使用 React 的 Context 相比,Zustand 提供了更简洁的 API。它消除了编写大量样板代码的需要,例如创建上下文提供者和消费者,从而减少了代码的复杂性。
- 只在变化时渲染组件:Zustand 使用浅比较来确定是否在状态更改时重新渲染组件。这意味着只有直接受到状态更新影响的组件将重新渲染,与使用 React 的 Context 相比,可以避免不必要的重新渲染,提高性能。
- 集中化、基于动作的状态管理:Zustand 鼓励集中化和基于动作的状态管理方式。它提供了内置功能来处理动作,允许您定义更新状态的动作和修改方法,以可预测和可控的方式更新状态。这使得更容易理解和维护应用程序的状态。
Recipes
获取所有数据
您可以这样做,但请记住这会导致组件在每次状态改变时进行更新!
jsx
const state = useBearStore()
选择多个状态片段
默认情况下,它使用严格相等性(old === new)来检测更改,这对于原子状态的选择是高效的。
jsx
const nuts = useBearStore((state) => state.nuts)
const honey = useBearStore((state) => state.honey)
如果您想构建一个包含多个状态选择的单个对象,类似于redux的mapStateToProps,您可以告诉zustand您希望使用浅层比较来对该对象进行差异化处理,通过传递浅相等性函数。
要使用自定义的相等性函数,您需要使用createWithEqualityFn而不是create。通常,您会希望将Object.is指定为默认相等性函数的第二个参数,但该函数是可配置的。
jsx
import { createWithEqualityFn } from 'zustand/traditional'
import { shallow } from 'zustand/shallow'
// 使用createWithEqualityFn而不是create
const useBearStore = createWithEqualityFn(
(set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}),
Object.is // 指定默认的相等性函数,可以是浅层比较
)
// 使用Object pick,当state.nuts或state.honey发生变化时重新渲染组件。
const { nuts, honey } = useBearStore(
(state) => ({ nuts: state.nuts, honey: state.honey }),
shallow
)
// 使用Array pick,当state.nuts或state.honey发生变化时重新渲染组件。
const [nuts, honey] = useBearStore(
(state) => [state.nuts, state.honey],
shallow
)
// 使用Mapped picks,当state.treats按顺序更改、计数或键时重新渲染组件。
const treats = useBearStore((state) => Object.keys(state.treats), shallow)
为了更精细地控制重新渲染,您可以提供任何自定义相等性函数。
jsx
const treats = useBearStore(
(state) => state.treats,
(oldTreats, newTreats) => compare(oldTreats, newTreats)
)
覆盖状态
set
函数有第二个参数,默认为false
。设置为false
时,它将替换状态模型而不是合并。请注意不要清除您所依赖的部分,比如操作(actions)。
jsx
import omit from 'lodash-es/omit'
const useFishStore = create((set) => ({
salmon: 1,
tuna: 2,
deleteEverything: () => set({}, true), // 清除整个存储空间,包括操作(actions)在内
deleteTuna: () => set((state) => omit(state, ['tuna']), true),
}))
异步操作
只需在准备就绪时调用set
,zustand不关心您的操作是异步还是同步的。
jsx
const useFishStore = create((set) => ({
fishies: {},
fetch: async (pond) => {
const response = await fetch(pond)
set({ fishies: await response.json() })
},
}))
在操作(actions)中从状态(state)中读取数据。
set
函数允许使用函数更新set(state => result)
,但是您仍然可以通过get
方法在函数外部访问状态(state)。
jsx
const useSoundStore = create((set, get) => ({
sound: 'grunt',
action: () => {
const sound = get().sound
// ...
},
}))
在组件之外读取/写入状态并对其变化做出反应
有时候您需要以非响应式的方式访问状态或者对存储进行操作。为了处理这些情况,生成的钩子(hook)附加了一些实用函数到其原型上。
jsx
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()
// 当然,您也可以像往常一样使用钩子
const Component = () => {
const paw = useDogStore((state) => state.paw)
...
如果您需要使用选择器(selector)进行订阅
如果您需要使用选择器来进行订阅,subscribeWithSelector
中间件将会很有帮助。
使用这个中间件,subscribe
方法可以接受额外的签名参数:
ts
subscribe(selector, callback, options?: { equalityFn, fireImmediately }): Unsubscribe
js
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
可以导入和使用不依赖于 React 的 Zustand 核心。唯一的区别是 create
函数不会返回一个 hook,而是返回 API 实用程序。
jsx
import { createStore } from 'zustand/vanilla'
const store = createStore(() => ({ ... }))
const { getState, setState, subscribe } = store
export default store
从版本4开始,您可以使用 useStore
hook 来使用原生的存储库。
jsx
import { useStore } from 'zustand'
import { vanillaStore } from './vanillaStore'
const useBoundStore = (selector) => useStore(vanillaStore, selector)
:warning:请注意,修改 set
或 get
的中间件不会应用于 getState
和 setState
。
瞬时更新(用于经常发生的状态更改)
subscribe
函数允许组件绑定到状态的一部分,而不会在更改时强制重新渲染。最好结合使用 useEffect
在组件卸载时自动取消订阅。当您被允许直接修改视图时,这可能会对性能产生重大影响。
jsx
const useScratchStore = create(set => ({ scratches: 0, ... }))
const Component = () => {
// Fetch initial state
const scratchRef = useRef(useScratchStore.getState().scratches)
// Connect to the store on mount, disconnect on unmount, catch state-changes in a reference
useEffect(() => useScratchStore.subscribe(
state => (scratchRef.current = state.scratches)
), [])
...
如果您受够了 reducers 和更改嵌套状态,可以使用 Immer!
减少嵌套结构是一项繁琐的任务。你尝试过Immer吗?
jsx
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()
Alternatively, there are some other solutions.
中间件
你可以根据你的喜好以任何方式对你的 Store 进行函数式组合。
jsx
// Log every time state is changed
const log = (config) => (set, get, api) =>
config(
(...args) => {
console.log(' applying', args)
set(...args)
console.log(' new state', get())
},
get,
api
)
const useBeeStore = create(
log((set) => ({
bees: false,
setBees: (input) => set({ bees: input }),
}))
)
持续的中间件
你可以使用任何类型的存储来持久化你的 Store 数据。
jsx
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', // unique name
storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used
}
)
)
See the full documentation for this middleware.
Immer 中间件
Immer也可以作为中间件使用。
jsx
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
const useBeeStore = create(
immer((set) => ({
bees: 0,
addBees: (by) =>
set((state) => {
state.bees += by
}),
}))
)
你觉得不能没有类似Redux的reducer和action类型吗?
jsx
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 进行了链接,设置了初始状态,并在状态本身和 Vanilla API 中添加了一个 dispatch 函数。
jsx
import { redux } from 'zustand/middleware'
const useGrumpyStore = create(redux(reducer, initialState))
Redux devtools
jsx
import { devtools } from 'zustand/middleware'
// 与普通操作存储一起使用,它将以"setState"记录操作。
const usePlainStore = create(devtools(store))
// 与redux存储一起使用,它将记录完整的操作类型
const useReduxStore = create(devtools(redux(reducer, initialState)))
一个redux devtools连接用于多个存储
jsx
import { devtools } from 'zustand/middleware'
// 与普通操作存储一起使用,它将以"setState"记录操作。
const usePlainStore1 = create(devtools(store, { name, store: storeName1 }))
const usePlainStore2 = create(devtools(store, { name, store: storeName2 }))
// 与redux存储一起使用,它将记录完整的操作类型
const useReduxStore = create(devtools(redux(reducer, initialState)), , { name, store: storeName3 })
const useReduxStore = create(devtools(redux(reducer, initialState)), , { name, store: storeName4 })
在 Redux DevTools 中,给不同的连接名称分配不同的值可以将存储分隔开。这也可以帮助将不同的存储组合成单独的 Redux DevTools 连接。
DevTools 接受存储函数作为第一个参数,可选地,你可以使用第二个参数来为存储命名或配置序列化选项。
为存储命名:devtools(store, { name: "MyStore" })
,这将在 DevTools 中创建一个名为 "MyStore" 的独立实例。
配置序列化选项:devtools(store, { serialize: { options: true } })
。
记录动作
DevTools 只会记录来自每个分离的存储的动作,而不是像典型的组合 reducer Redux 存储那样。可以通过以下方式记录每个 set 函数的特定动作类型:
jsx
const createBearSlice = (set, get) => ({
eatFish: () =>
set(
(prev) => ({ fishes: prev.fishes > 1 ? prev.fishes - 1 : 0 }),
false,
'bear/eatFish'
),
})
你还可以将动作的类型和载荷一起记录:
jsx
const createBearSlice = (set, get) => ({
addFishes: (count) =>
set((prev) => ({ fishes: prev.fishes + count }), false, {
type: 'bear/addFishes',
count,
}),
})
如果没有提供动作类型,则默认为 "anonymous"。你可以通过提供 anonymousActionType
参数来自定义此默认值:
jsx
devtools(..., { anonymousActionType: 'unknown', ... })
如果您希望禁用devtools(例如在生产环境中)。您可以通过提供enabled
参数来自定义此设置:
jsx
devtools(..., { enabled: false, ... })
React context
使用 create
创建的存储不需要上下文提供程序。在某些情况下,你可能希望使用上下文进行依赖注入或者如果你想使用来自组件的 props 初始化存储。因为常规存储是一个 hook,将其作为正常上下文值传递可能会违反 hook 的规则。
自 v4 开始推荐的方法是使用 Vanilla Store。
jsx
import { createContext, useContext } from 'react'
import { createStore, useStore } from 'zustand'
const store = createStore(...) // vanilla store without hooks
const StoreContext = createContext()
const App = () => (
<StoreContext.Provider value={store}>
...
</StoreContext.Provider>
)
const Component = () => {
const store = useContext(StoreContext)
const slice = useStore(store, selector)
...
TypeScript Usage
基本的typescript用法不需要任何特殊的东西,除了写create<State>()(...)
而不是create(...)
。
ts
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
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',
}
)
)
)