Published 2022-11-23

Zustand-入门

全局(客户端)状态管理并不总是像今天这样。我清楚地记得有一段时间,在高阶组件中的状态管理最佳选择是使用Reduxconnect 连接 mapStateToPropsmapDispatchToProps

即使是context api,最初也不是那么符合需求 ,因为刚出的时候只支持自身props

当然,hooks 出现后一切都变了。不仅现有的解决方案变得更容易使用,而且新的解决方案也诞生了。

Zustand

其中增长迅速的像Zustand,它非常小而精悍,提供了简易api创建全局状态以及通过selectors过来状态。它类似Redux,又比Redux简易。

就像ReactZustand提供丰富的扩展,可以结合immer,也可以使用其他middlewares

ZustandcreateStore 函数的返回值作为一个自定义 hook 来实现,其中为了让 React 组件能感知到状态更新,是利用 useEffect 来完成订阅操作,而状态更新发布后,则通过 forceUpdate() 来强制组件进行 rerender 以获取最新的状态。

实际上,用法和 react-redux 非常相似,但获取状态与更新状态均只需要使用 useStore 一个 API 即可完成业务。

我从 2018 年开始在大小项目中使用Zustand,我也为Zustand开源做过贡献,以下是一些在实践中积累的技巧。

仅导出自定义钩子

这是首要技巧,对于任何 React 开发都满足,我之前列出了很多Hooks 优点

// ⬇️ not exported, so no one can subscribe to the whole store
const useBearStore = create((set) => ({
  bears: 0,
  fish: 0,
  increasePopulation: (by) => set((state) => ({bears: state.bears + by})),
  eatFish: () => set((state) => ({fish: state.fish - 1})),
  removeAllBears: () => set({bears: 0})
}))

// 💡 exported - consumers don't need to write selectors
export const useBears = () => useBearStore((state) => state.bears)

它们将为您提供更清晰的界面,并且您不需要在所有您希望只订阅状态的一个值的地方编写选择器。此外,它还避免了意外地订阅整个状态:

// ❌ we could do this if useBearStore was exported
const {bears} = useBearStore()

虽然结果可能是相同的(你会得到 bear 的数量) ,上面的代码将订阅你的整个状态,这意味着你的组件将在某个状态更新后全部重新渲染 ,即使 bear 没有改变,比如,fish 的状态改变了。SelectorsZustand 是可选择的,但对我来说,他们真的不是。即使我们有一个只有一个状态值的存储,我也会编写自定义钩子,以便将来能够添加更多的状态。

多使用原子选择器

这个在官方文档也解释了,所以我会尽量简短,但是非常重要,因为错用、滥用它可能会导致渲染性能下降。

Zustand 通过比较选择器的结果和之前渲染的结果来决定何时通知组件更新。默认情况下,它通过严格的相等检查来完成。

实际上,这意味着选择器必须返回稳定的结果。如果返回一个新的 ArrayObject,它将始终被视为更改,即使内容相同:

// 🚨 selector returns a new Object in every invocation
const {bears, fish} = useBearStore((state) => ({
  bears: state.bears,
  fish: state.fish
}))

// 😮 so these two are equivalent
const {bears, fish} = useBearStore()

如果要从选择器返回 Object 或 Array,可以将比较函数调整为使用浅比较:

import shallow from 'zustand/shallow'

// ⬇️ much better, because optimized
const {bears, fish} = useBearStore(
  (state) => ({bears: state.bears, fish: state.fish}),
  shallow
)

然而我更倾向于导出两个独立的selectors

export const useBears = () => useBearStore((state) => state.bears)
export const useFish = () => useBearStore((state) => state.fish)

假如组件需要多个值,引入多个 hooks 即可。

actions 和 state 分离

actions 是不变的静态函数,它更新 state 中的值,所以它们不是真正的“状态”。将它们作为一个额外的对象在我们的存储中分离将允许我们将它们作为一个钩子暴露出来,然后我们可以在组件中使用它们:

const useBearStore = create((set) => ({
  bears: 0,
  fish: 0,
  // ⬇️ separate "namespace" for actions
  actions: {
    increasePopulation: (by) => set((state) => ({bears: state.bears + by})),
    eatFish: () => set((state) => ({fish: state.fish - 1})),
    removeAllBears: () => set({bears: 0})
  }
}))

export const useBears = () => useBearStore((state) => state.bears)
export const useFish = () => useBearStore((state) => state.fish)

// 🎉 one selector for all our actions
export const useBearActions = () => useBearStore((state) => state.actions)

现在可以只使用一个 hooks 导出所有的 actions

const {increasePopulation} = useBearActions()

这看起来和上面提到的原子选择器相反,但是事实并非如此,由于 actions 不会改变,actions 可以看出是一个原子块。

Model Actions as Events, not Setters

这是一个通用的提示,无论你是否使用 useReducerReduxZustand。 事实上,这个技巧直接来自 Redux 风格指南。 它将帮助您将业务逻辑保留在存储中,而不是保留在组件中。 上面的例子已经使用了这种模式——逻辑(例如“增加人口”)存在于状态中。 组件只是调用操作,而存储决定如何处理它。

保持状态小型化

Redux 不同的是,Zustand 鼓励您拥有多个小型状态。每个状态可以对单个状态负责。如果你需要组合它们,你可以使用自定义钩子:

const currentUser = useCredentialsStore((state) => state.currentUser)
const user = useUsersStore((state) => state.users[currentUser])

注意: Zustand 确实有另外一种将状态组合成切片的方法,但是我从来不需要这种方法。对我来说,它看起来不是很简单,尤其是涉及到 TypeScript 的时候。如果我真的需要它,我可能会选择 Redux Toolkit。

结合其他库

老实说,我并不经常需要组合多个 Zustand 状态,因为应用程序中的大多数状态要么是服务器状态,要么是 URL 状态。与将两个状态组合起来相比,我更有可能将 Zustand 状态与 useQueryuseParams 结合起来。不过,同样的原则也适用于: 如果你需要将另一个钩子与 Zustand 状态结合起来,自定义钩子也许是你最好的选择:

const useFilterStore = create((set) => ({
  applied: [],
  actions: {
    addFilter: (filter) =>
      set((state) => ({applied: [...state.applied, filter]}))
  }
}))

export const useAppliedFilters = () => useFilterStore((state) => state.applied)

export const useFiltersActions = () => useFilterStore((state) => state.actions)

// 🚀 combine the zustand store with a query
export const useFilteredTodos = () => {
  const filters = useAppliedFilters()
  return useQuery({
    queryKey: ['todos', filters],
    queryFn: () => getTodos(filters)
  })
}

在这里,应用的筛选器驱动查询,因为筛选器是查询键的一部分。 每次调用 addFilter (可以在 UI 中的任何地方执行)时,都会自动触发一个新查询,这个查询也可以在 UI 中的任何地方使用。 我觉得这是一个非常简洁明了的说明,但又不会太神奇。