zustand的中文文档,中文入门手册

一个小巧、快速且可扩展的精简状态管理解决方案,使用简化的 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:请注意,修改 setget 的中间件不会应用于 getStatesetState

瞬时更新(用于经常发生的状态更改)

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',
      }
    )
  )
)
相关推荐
PleaSure乐事1 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
getaxiosluo1 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v1 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
栈老师不回家2 小时前
Vue 计算属性和监听器
前端·javascript·vue.js
前端啊龙2 小时前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js
小远yyds2 小时前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
程序媛小果3 小时前
基于java+SpringBoot+Vue的宠物咖啡馆平台设计与实现
java·vue.js·spring boot
小光学长3 小时前
基于vue框架的的流浪宠物救助系统25128(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。
数据库·vue.js·宠物
阿伟来咯~3 小时前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端3 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js