zustand

Image description

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相等函数来对对象进行浅差异。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

import shallow from 'zustand/shallow'

// 对象选择,当 `state.nuts` 或 `state.honey` 改变时重新渲染组件
const { nuts, honey } = useStore(state => ({ nuts: state.nuts, honey: state.honey }), shallow)

// 数组选择,当 `state.nuts` 或 `state.honey` 改变时重新渲染组件
const [nuts, honey] = useStore(state => [state.nuts, state.honey], shallow)

// 映射选择,当 `state.treats` 按 `count` 或 `keys` 顺序改变时重新渲染组件
const treats = useStore(state => Object.keys(state.treats), 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

1
2
3
4
5
6
7
8
9

import omit from "lodash-es/omit"

const useStore = create(set => ({
  salmon: 1,
  tuna: 2,
  deleteEverything: () => set({ }, true), // 清除整个存储,包括操作
  deleteTuna: () => set(state => omit(state, ['tuna']), true)
}))

六、异步操作(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进行操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19

const useStore = create(() => ({ paw: true, snout: true, fur: true }))

// 在React组件之外获取state
const paw = useStore.getState().paw //true

// 侦听所有更改,在每次更改时同步触发
const unsub = useStore.subscribe(console.log)
// 更新状态,将触发侦听器
useStore.setState({ paw: false })
// 取消订阅侦听
unsub()

// 销毁商店(删除所有侦听器)
useStore.destroy()

// 当然可以像hook一样使用
function Component() {
  const paw = useStore(state => state.paw)

2. 使用订阅处理器(中间件)

如果您需要使用处理器订阅,subscribeWithSelector 中间件会有所帮助。

有了这个中间件,subscribe 接受一个额外的签名:

subscribe(selector, callback, options?: { equalityFn, fireImmediately }): Unsubscribe
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12

import { subscribeWithSelector } from 'zustand/middleware'
const useStore = create(subscribeWithSelector(() => ({ paw: true, snout: true, fur: true })))

// 订阅状态值变化
const unsub2 = useStore.subscribe(state => state.paw, console.log)
// 订阅上一个状态值
const unsub3 = useStore.subscribe(state => state.paw, (paw, previousPaw) => console.log(paw, previousPaw))
// 订阅一个可选的浅差异方法
const unsub4 = useStore.subscribe(state => [state.paw, state.fur], console.log, { equalityFn: shallow })
// 解除订阅
const unsub5 = useStore.subscribe(state => state.paw, console.log, { fireImmediately: true })

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

// 记录每次改变的state
const log = config => (set, get, api) => config(args => {
  console.log("  applying", args)
  set(args)
  console.log("  new state", get())
}, get, api)

// 将 set 方法变成一个 immer proxy
const immer = config => (set, get, api) => config((partial, replace) => {
  const nextState = typeof partial === 'function'
      ? produce(partial)
      : partial
  return set(nextState, replace)
}, get, api)

const useStore = create(
  log(
    immer((set) => ({
      bees: false,
      setBees: (input) => set((state) => void (state.bees = input)),
    })),
  ),
)

2. 管理中间件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

import create from "zustand"
import produce from "immer"
import pipe from "ramda/es/pipe"

/* 上一个例子中的日志和immer函数 */
/* 通过pipe集合任意数量的中间件 */
const createStore = pipe(log, immer, create)

const useStore = createStore(set => ({
  bears: 1,
  increasePopulation: () => set(state => ({ bears: state.bears + 1 }))
}))

export default useStore

3. 在管理中间件中使用TS

yarn add --dev @types/ramda
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

import create from "zustand"
import { devtools, redux } from "zustand/middleware"
import pipe from "ramda/es/pipe"

const log: typeof devtools = config => (set, get, api) =>
  config(
    args => {
      console.log("  applying", args)
      set(args)
      console.log("  new state", get())
    },
    get,
    api
  )

export type State = {
  grumpiness: number
}

const initialState: State = {
  grumpiness: 0,
}

const createStore = pipe(redux, devtools, log, create)

enum types {
  increase = "INCREASE",
  decrease = "DECREASE",
}

const reducer = (
  state: State,
  { type, by = 1 }: { type: types; by: number }
) => {
  switch (type) {
    case types.increase:
      return { grumpiness: state.grumpiness + by }
    case types.decrease:
      return { grumpiness: state.grumpiness - by }
  }
}

const useStore = createStore(reducer, initialState)

export default useStore

十二、状态持久化中间件 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. 选项

  1. name 这是唯一需要的选项。给定的名称将是用于存储state的键,因此它必须是唯一的。

  2. getStorage 默认: () => localStorage

给定的存储必须与以下接口匹配:

1
2
3
4
5
6

interface Storage {
  getItem: (name: string) => string | null | Promise<string | null>
  setItem: (name: string, value: string) => void | Promise<void>
  removeItem: (name: string) => void | Promise<void>
}
  1. 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函数才能使其正常工作。见下文

  1. 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)),
  }
))
  1. 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 })
  }
))
  1. onRehydrateStorage (水和存储)
  • Schema: (state: Object) => ((state?: Object, error?: Error) => void) | void

此选项能够在水合存储时调用的侦听器函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21

export const useStore = create(persist(
  (set, get) => ({
    // ...
  }),
  {
    // ...
    onRehydrateStorage: (state) => {
      console.log("hydration starts"); //水合开始
			
      // 可选
      return (state, error) => {
        if (error) {
          console.log("an error happened during hydration", error) //水合过程中发生错误
        } else {
          console.log("hydration finished") //水合完成
        }
      }
    }
  }
))
  1. version 指定存储版本
  • Schema: number
  • Default: 0

如果在 storage 中引入重大更改(例如重命名字段),可以指定新版本号。
默认情况下,如果 storage 中的版本与代码中的版本不匹配,则不会使用 storage 的值。

有关处理重大更改的更多详细信息,请参阅下面的选项migrate

  1. migrate 版本迁移
  • Schema: (persistedState: Object, version: number) => Object | Promise<Object>
  • Default: (persistedState) => persistedState

可以使用此选项来处理版本迁移。migrate 函数将持久化state和版本号作为参数。
它必须返回符合最新版本(代码中的版本)的state。

例如,如果要重命名字段,可以使用以下命令:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19

export const useStore = create(persist(
  (set, get) => ({
    newField: 0, // 假设该字段在版本 0 中以其他方式命名
  }),
  {
    // ...
    version: 1, // 如果 storage 中的版本与此迁移不匹配,则会触发:
    migrate: (persistedState, version) => {
      if (version === 0) {
        // 如果存储的值在版本为 0 ,则将该字段重命名为新名称
        persistedState.newField = persistedState.oldField;
        delete persistedState.oldField;
      }
      
      return persistedState;
    },
  }
))
  1. 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 组件的内部或外部与持久中间件进行大量交互。

  1. setOptions 更改中间件选项
  • Schema: (newOptions: PersistOptions) => void

此方法能够更改中间件选项。请注意,新选项将与当前选项合并。

例如,这可用于更改 storage 名称:

useStore.persist.setOptions({
  name: "new-name"
});

甚至更改 storage 引擎:

useStore.persist.setOptions({
  getStorage: () => sessionStorage,
});
  1. clearStorage
  • Schema: () => void

这可用于完全清除 storage 中的持久值。

useStore.persist.clearStorage();
  1. rehydrate 触发水合
  • Schema: () => Promise<void>

在某些情况下,可能希望手动触发水合。可以通过调用该rehydrate方法来完成。

await useStore.persist.rehydrate();
  1. hasHydrated 获取水合状态
  • Schema: () => boolean

这是一个React的 getter,用于了解存储是否已被水合(请注意,在调用useStore.persist.rehydrate()时会更新)。

useStore.persist.hasHydrated();
  1. onHydrate 水合过程开始
  • Schema: (listener: (state) => void) => () => void

水合过程开始时将调用给定的侦听器。

1
2
3
4
5
6
7

const unsub = useStore.persist.onHydrate((state) => {
  console.log("hydration starts");
});

// 稍后...
unsub();
  1. onFinishHydration 水合过程结束
  • Schema: (listener: (state) => void) => () => void

当水化过程结束时,将调用给定的侦听器。

1
2
3
4
5
6
7

const unsub = useStore.persist.onFinishHydration((state) => {
  console.log("hydration finished");
});

// 稍后...
unsub();

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. 还可以创建自定义useHydrationhook:

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 不匹配,可以创建自己的存储:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

import create from "zustand"
import { persist, StateStorage } from "zustand/middleware"
import { get, set, del } from 'idb-keyval' // 可以使用任何东西:IndexedDB、Ionic Storage 等

// 自定义 storage 对象
const storage: StateStorage = {
  getItem: async (name: string): Promise<string | null> => {
    console.log(name, "has been retrieved");
    return (await get(name)) || null
  },
  setItem: async (name: string, value: string): Promise<void> => {
    console.log(name, "with value", value, "has been saved");
    await set(name, value)
  },
  removeItem: async (name: string): Promise<void> => {
    console.log(name, "has been deleted");
    await del(name)
  }
}

export const useStore = create(persist(
  (set, get) => ({
    fishes: 0,
    addAFish: () => set({ fishes: get().fishes + 1 })
  }),
  {
    name: "food-storage", //唯一名称
    getStorage: () => storage,
  }
))

十三、像 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函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

import React, { useEffect } from 'react'
import ReactDOM from 'react-dom'
import create from 'zustand'
import { devtools, redux } from 'zustand/middleware'
import './styles.css'

const initialState = { count: 0 }
const types = { increase: 'INCREASE', decrease: 'DECREASE' }
const reducer = (state, { type, by }) => {
  switch (type) {
    case types.increase:
      return { count: state.count + by }
    case types.decrease:
      return { count: state.count - by }
    default:
      return
  }
}

const [useStore, api] = create(
  // 将 store 连接到 devtools
  // 如果没有 reducers 和 action-types,你会看到“setState”被注销
  devtools(
    // 将我们的 store 转换为 redux action dispatcher ...
    // 向 store 添加一个 dispatch 方法
    redux(reducer, initialState)
  )
)

function Counter() {
  const count = useStore(state => state.count)
  useEffect(() => {
    // Increase
    setTimeout(() => api.dispatch({ type: types.increase, by: 3 }), 1000)
    // Decrease
    setTimeout(() => api.dispatch({ type: types.decrease, by: 1 }), 2000)
    // Decrease
    setTimeout(() => api.dispatch({ type: types.decrease, by: 1 }), 3000)
  }, [])
  return <span class="header">{count}</span>
}

ReactDOM.render(<Counter />, document.getElementById('root'))

十四、在 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 开发工具

1
2
3
4
5
6
7
8
9

import { devtools } from 'zustand/middleware'

// 使用普通操作存储,它将记录操作为“setState”
// devtools 将只记录来自每个单独存储的操作,这与典型的 redux 存储不同
const useStore = create(devtools(store))

// 使用 redux 存储,它将记录完整操作类型
const useStore = create(devtools(redux(reducer, initialState)))

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. 在组件中使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

import create from "zustand";
import createContext from "zustand/context";

// 最佳实践:可以将下面的 createContext() 和 createStore 移动到单独的文件 (store.js) 并导入提供程序Provider,在此处/任何需要的地方使用 store。

const { Provider, useStore } = createContext();

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

const Button = () => {
  return (
      {/** store() - 每次使用 Button 组件都创建一个 store,而不是为所有组件使用一个 store **/}
    <Provider createStore={createStore}> 
      <ButtonChild />
    </Provider>
  );
};

const ButtonChild = () => {
  const state = useStore();
  return (
    <div>
      {state.bears}
      <button
        onClick={() => {
          state.increasePopulation();
        }}
      >
        +
      </button>
    </div>
  );
};

export default function App() {
  return (
    <div className="App">
      <Button />
      <Button />
    </div>
  );
}

3. createContext 使用 props 初始化(在 TS 中)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

import create from "zustand";
import createContext from "zustand/context";

type BearState = {
  bears: number
  increase: () => void
}

// 将类型传递给 `createContext` 而不是 `create`
const { Provider, useStore } = createContext<BearState>();

export default function App({ initialBears }: { initialBears: number }) {
  return (
    <Provider
      createStore={() =>
        create((set) => ({
          bears: initialBears,
          increase: () => set((state) => ({ bears: state.bears + 1 })),
        }))
      }
    >
      <Button />
    </Provider>
)
}

十七、TypeScript 类型定义

1. 类型定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18

// 可以使用 `type`
type BearState = {
  bears: number
  increase: (by: number) => void
}

// 或者 `interface`
interface BearState {
  bears: number
  increase: (by: number) => void
}

// 它对两者都有效
const useStore = create<BearState>(set => ({
  bears: 0,
  increase: (by) => set(state => ({ bears: state.bears + by })),
}))

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)

目录结构

Image description

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