Project Icon

zustand

轻量级React状态管理库 基于Hooks的简洁API

zustand是一个轻量级的React状态管理库,采用简化的flux原则。它提供基于hooks的简洁API,无需样板代码。zustand支持异步操作、状态持久化和中间件扩展,适用于各种React应用。相比Redux和Context API,zustand更简洁高效,是React状态管理的理想选择。

构建状态 构建大小 版本 下载量 Discord 频道

一个小巧、快速且可扩展的基础状态管理解决方案,采用简化的 flux 原则。它具有基于钩子的舒适 API,不臃肿也不固执己见。

不要因为它看起来可爱就轻视它。它有相当强大的功能,花费了大量时间来处理常见的陷阱,如令人恐惧的僵尸子问题React 并发和混合渲染器之间的上下文丢失。它可能是 React 生态系统中唯一一个正确处理所有这些问题的状态管理器。

你可以在这里尝试实时演示。

npm i zustand

:warning: 本说明文档是为 JavaScript 用户编写的。如果你是 TypeScript 用户,请务必查看我们的TypeScript 使用部分

首先创建一个 store

你的 store 是一个钩子!你可以在其中放置任何内容:原始类型、对象、函数。状态必须以不可变的方式更新,set 函数合并状态以帮助实现这一点。

import { create } from 'zustand'

const useBearStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}))

然后绑定你的组件,就这么简单!

在任何地方使用这个钩子,不需要提供者。选择你的状态,组件将在变化时重新渲染。

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>
}

为什么选择 zustand 而不是 redux?

为什么选择 zustand 而不是 context?

  • 更少的样板代码
  • 只在变化时渲染组件
  • 集中式、基于动作的状态管理

使用方法

获取所有内容

你可以这样做,但请记住,这将导致组件在每次状态变化时更新!

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) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}))

// 对象选择,当 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)))

为了更好地控制重渲染,你可以提供任何自定义的相等性函数。

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() })
  },
}))

在actions中读取状态

set 允许函数更新 set(state => result),但你仍然可以通过 get 在外部访问状态。

const useSoundStore = create((set, get) => ({
  sound: 'grunt',
  action: () => {
    const sound = get().sound
    ...

在组件外部读写状态并响应变化

有时你需要以非响应式的方式访问状态或对store进行操作。对于这些情况,生成的hook在其原型上附加了实用函数。

:warning: 不推荐在 React 服务器组件 中使用这种技术来添加状态(通常在 Next.js 13 及以上版本中)。这可能会导致意外的错误和用户隐私问题。更多详情请参见 #2200

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)
  ...

使用带选择器的subscribe

如果你需要使用选择器进行订阅,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)
// subscribe 还会暴露前一个值
const unsub3 = useDogStore.subscribe(
  (state) => state.paw,
  (paw, previousPaw) => console.log(paw, previousPaw),
)
// subscribe 还支持可选的相等性函数
const unsub4 = useDogStore.subscribe(
  (state) => [state.paw, state.fur],
  console.log,
  { equalityFn: shallow },
)
// 订阅并立即触发
const unsub5 = useDogStore.subscribe((state) => state.paw, console.log, {
  fireImmediately: true,
})

在不使用 React 的情况下使用 zustand

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 来使用原生 store。

import { useStore } from 'zustand'
import { vanillaStore } from './vanillaStore'

const useBoundStore = (selector) => useStore(vanillaStore, selector)

:warning: 请注意,修改 setget 的中间件不会应用于 getStatesetState

瞬态更新(用于频繁发生的状态变化)

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)
  ), [])
  ...

厌倦了 reducers 和修改嵌套状态?使用 Immer!

减少嵌套结构是令人疲惫的。你试过 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()

另外,还有一些其他解决方案。

Persist 中间件

你可以使用任何类型的存储来持久化你的 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 中间件

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
      }),
  })),
)

离不开类似 redux 的 reducers 和 action types?

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函数添加到状态本身和原生API中。

```jsx
import { redux } from 'zustand/middleware'

const useGrumpyStore = create(redux(reducer, initialState))

Redux开发工具

安装Redux DevTools Chrome扩展以使用开发工具中间件。

import { devtools } from 'zustand/middleware'

// 用于普通action store,它会将actions记录为"setState"
const usePlainStore = create(devtools((set) => ...))
// 用于redux store,它会记录完整的action类型
const useReduxStore = create(devtools(redux(reducer, initialState)))

多个store使用一个redux开发工具连接

import { devtools } from 'zustand/middleware'

// 用于普通action store,它会将actions记录为"setState"
const usePlainStore1 = create(devtools((set) => ..., { name, store: storeName1 }))
const usePlainStore2 = create(devtools((set) => ..., { name, store: storeName2 }))
// 用于redux store,它会记录完整的action类型
const useReduxStore = create(devtools(redux(reducer, initialState)), , { name, store: storeName3 })
const useReduxStore = create(devtools(redux(reducer, initialState)), , { name, store: storeName4 })

分配不同的连接名称将在redux开发工具中分离store。这还有助于将不同的store分组到单独的redux开发工具连接中。

devtools将store函数作为其第一个参数,可选地,你可以通过第二个参数命名store或配置序列化选项。

命名store:devtools(..., {name: "MyStore"}),这将在开发工具中创建一个名为"MyStore"的单独实例。

序列化选项:devtools(..., { serialize: { options: true } })

记录Actions

devtools只会记录每个分离store的actions,不像典型的_combined reducers_ redux store。查看组合store的方法 https://github.com/pmndrs/zustand/issues/163

你可以通过传递第三个参数为每个set函数记录特定的action类型:

const useBearStore = create(devtools((set) => ({
  ...
  eatFish: () => set(
    (prev) => ({ fishes: prev.fishes > 1 ? prev.fishes - 1 : 0 }),
    undefined,
    'bear/eatFish'
  ),
  ...

你还可以记录action的类型及其payload:

  ...
  addFishes: (count) => set(
    (prev) => ({ fishes: prev.fishes + count }),
    undefined,
    { type: 'bear/addFishes', count, }
  ),
  ...

如果未提供action类型,则默认为"anonymous"。你可以通过提供anonymousActionType参数来自定义此默认值:

devtools(..., { anonymousActionType: 'unknown', ... })

如果你希望禁用devtools(例如在生产环境中)。你可以通过提供enabled参数来自定义此设置:

devtools(..., { enabled: false, ... })

React上下文

使用create创建的store不需要上下文提供者。在某些情况下,你可能想使用上下文进行依赖注入,或者如果你想用组件的props初始化store。因为普通store是一个钩子,将其作为普通上下文值传递可能会违反钩子规则。

自v4以来推荐的方法是使用原生store。

import { createContext, useContext } from 'react'
import { createStore, useStore } from 'zustand'

const store = createStore(...) // 不带钩子的原生store

const StoreContext = createContext()

const App = () => (
  <StoreContext.Provider value={store}>
    ...
  </StoreContext.Provider>
)

const Component = () => {
  const store = useContext(StoreContext)
  const slice = useStore(store, selector)
  ...

TypeScript使用

基本的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指南在这里

最佳实践

第三方库

一些用户可能想要扩展Zustand的功能集,这可以通过社区制作的第三方库来实现。有关Zustand的第三方库信息,请访问文档

与其他库的比较

项目侧边栏1项目侧边栏2
推荐项目
Project Cover

豆包MarsCode

豆包 MarsCode 是一款革命性的编程助手,通过AI技术提供代码补全、单测生成、代码解释和智能问答等功能,支持100+编程语言,与主流编辑器无缝集成,显著提升开发效率和代码质量。

Project Cover

AI写歌

Suno AI是一个革命性的AI音乐创作平台,能在短短30秒内帮助用户创作出一首完整的歌曲。无论是寻找创作灵感还是需要快速制作音乐,Suno AI都是音乐爱好者和专业人士的理想选择。

Project Cover

有言AI

有言平台提供一站式AIGC视频创作解决方案,通过智能技术简化视频制作流程。无论是企业宣传还是个人分享,有言都能帮助用户快速、轻松地制作出专业级别的视频内容。

Project Cover

Kimi

Kimi AI助手提供多语言对话支持,能够阅读和理解用户上传的文件内容,解析网页信息,并结合搜索结果为用户提供详尽的答案。无论是日常咨询还是专业问题,Kimi都能以友好、专业的方式提供帮助。

Project Cover

阿里绘蛙

绘蛙是阿里巴巴集团推出的革命性AI电商营销平台。利用尖端人工智能技术,为商家提供一键生成商品图和营销文案的服务,显著提升内容创作效率和营销效果。适用于淘宝、天猫等电商平台,让商品第一时间被种草。

Project Cover

吐司

探索Tensor.Art平台的独特AI模型,免费访问各种图像生成与AI训练工具,从Stable Diffusion等基础模型开始,轻松实现创新图像生成。体验前沿的AI技术,推动个人和企业的创新发展。

Project Cover

SubCat字幕猫

SubCat字幕猫APP是一款创新的视频播放器,它将改变您观看视频的方式!SubCat结合了先进的人工智能技术,为您提供即时视频字幕翻译,无论是本地视频还是网络流媒体,让您轻松享受各种语言的内容。

Project Cover

美间AI

美间AI创意设计平台,利用前沿AI技术,为设计师和营销人员提供一站式设计解决方案。从智能海报到3D效果图,再到文案生成,美间让创意设计更简单、更高效。

Project Cover

AIWritePaper论文写作

AIWritePaper论文写作是一站式AI论文写作辅助工具,简化了选题、文献检索至论文撰写的整个过程。通过简单设定,平台可快速生成高质量论文大纲和全文,配合图表、参考文献等一应俱全,同时提供开题报告和答辩PPT等增值服务,保障数据安全,有效提升写作效率和论文质量。

投诉举报邮箱: service@vectorlightyear.com
@2024 懂AI·鲁ICP备2024100362号-6·鲁公网安备37021002001498号