我用过或了解过前端业界大部分流行的状况办理库。他们有的很杂乱,有的很简略。有的用了一些深度改造的手法来优化细节,有的则是平淡无奇的告知所有运用者发生了变化。在技能方案怪异多变与层出不穷的当下,只有一个状况办理库让我深深着迷,她极度精简到让我觉得不能再简略了,可是她也满足完备到应对任何场景。而我就一直在追究这样的一个宛如艺术品一样的状况办理库,经过一段时间的运用,我很确认她便是我的梦中情库。

她的名字叫 zustand

Github: github.com/pmndrs/zust…

极简界说

咱们先看看其他业界的状况办理库的运用方法:

以比较主流的reduxmobx为例, 这儿直接仿制了官网的最小示例。

redux(@reduxjs/toolkit)

import { createSlice, configureStore } from "@reduxjs/toolkit";
const counterSlice = createSlice({
  name: "counter",
  initialState: {
    value: 0,
  },
  reducers: {
    incremented: (state) => {
      state.value += 1;
    },
    decremented: (state) => {
      state.value -= 1;
    },
  },
});
export const { incremented, decremented } = counterSlice.actions;
const store = configureStore({
  reducer: counterSlice.reducer,
});
store.subscribe(() => console.log(store.getState()));
store.dispatch(incremented());
store.dispatch(incremented());
store.dispatch(decremented());

mobx

import React from "react";
import ReactDOM from "react-dom";
import { makeAutoObservable } from "mobx";
import { observer } from "mobx-react";
class Timer {
  secondsPassed = 0;
  constructor() {
    makeAutoObservable(this);
  }
  increase() {
    this.secondsPassed += 1;
  }
  reset() {
    this.secondsPassed = 0;
  }
}
const myTimer = new Timer();
// Build a "user interface" that uses the observable state.
const TimerView = observer(({ timer }) => (
  <button onClick={() => timer.reset()}>
    Seconds passed: {timer.secondsPassed}
  </button>
));
ReactDOM.render(<TimerView timer={myTimer} />, document.body);
// Update the 'Seconds passed: X' text every second.
setInterval(() => {
  myTimer.increase();
}, 1000);

redux 中,咱们需求先构建 reducer 来记录如何处理状况,然后根据reducer构建一个store。取值是经过store来获取,修正则需求经过 根据reducer一一对应的action来修正。
这样就保证了数据流永远是单向活动的: 即 UI -> action -> reducer -> state -> UI 这样的进程。其呼应式的完成便是在执行action -> reducer的进程中搜集到了变化,然后通知所有订阅这个store的所有人。然后订阅者再经过名为selector的函数来比对变更决议本身是否要更新。

如:

const Foo = () => {
  useSelector(state => state.count.value)
}

咱们再来看看另一派的完成: mobx 界说了一个 class 作为存储数据的store, 而对于数据的任何修正都是用一种类似原生的方法 —— 直接赋值来完成的。即既能够直接访问store中修正里面的值也能够经过调用store暴露出的方法来修正数据。而数据的取值也是直接经过最简略的数据访问来完成的。
看上去十分美好,可是这是经过一些”黑魔法”来完成的,当执行makeAutoObservable(this)的那一刻,本来的成员变量已经不是本来的数据了,已经变成了由mobx包裹了一层完成的 可观察目标, 即这些目标的赋值与取值都不是本来的含义了。这也便是为什么mobx能够完成reactive呼应式的原因。

这时分咱们再来看看zustand是怎样做的:

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} around here ...</h1>
}
function Controls() {
  const increasePopulation = useBearStore((state) => state.increasePopulation)
  return <button onClick={increasePopulation}>one up</button>
}

是的,只需求简略的一个目标就界说了一个store,不需求特意去区分是state仍是action, 也不需求特意去结构一个 class 来做的十分臃肿。zustand 便是用最简略的规划去做一些工作,乃至其中心代码只有500行不到。

redux本身最中心的代码只有200行左右,可是假如要在react中运用需求加上redux-react@reduxjs/toolkit 就远远超过了

别的能够注意到的是,zustand天然生成规划了一种场景便是react环境。其他”有野心”的状况办理库往往是从 vanilla 环境(纯js环境)开始规划,然后增加了对react的支撑,或许后续还会增加其他框架的支撑。可是zustand则不是,天然生成支撑了react环境,然后根据react环境再衍生出vanilla环境的支撑。

那么很多人就会好奇,已然都支撑vanillareact,那么从哪个环境开始规划有什么区别么?

答案是有的,从不同的环境开始会从底层规划上就带来很大的误差,最后落地到运用方来说便是根本运用需求调用的代码、运行时以及杂乱度的差异。在我过去的开发经验告知我这样是正确的,我几乎没有看见过哪个库能同时在多个框架中都能如鱼得水的。不同的框架会有不同的生态,而哪些特有的生态则是最贴合的,如redux之于reactpinia之于vue, rxjs之于Angular。很少有哪个库能够在多个环境中”巴结”的。因此zustand就一种十分聪明的做法,专心于一点十分重要。

那么回到zustand的根本运用,咱们能够看到zustand经过create导出的是一个 react hook, 经过这个hook 咱们能够直接拿到store里面的stateaction,十分类似于reduxuseSelector。不同的是不需求dispatch来推送action, 也没有任何模板代码,数据类型天然生成区分了stateaction, 只需求最简略的调用即可。

比较于mobx, 也没有什么”黑魔法”, 简略而不容易出错。而且也不像mobx会由于依靠class完成的store而引进天然的问题(比方作为数据store不该该有生命周期,而classconstructor天然生成就成为了生命周期的一种)

人的惊骇往往来自未知,mobx 的目标便是这样的一个黑盒。这便是我不怎样喜爱 mobx的原因

那么,怎样应用到所有场景呢

zustand是一种十分简略的完成,简略到让人觉得是不是总有一些场合是无法掩盖到的。而这便是我觉得zustand是一件艺术品的原因。由于他总有巧妙的方法来不失高雅的适配任何我想要的场景。

在纯js中调用? 能够

useBearStore.getState()

经过getState方法就能够获取最新的状况,在运用的进程中需求注意这是一个函数,意图是在运行时中获取到最新的值。里面的数据不是reactive的。

想要有效果域? 能够

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

与原生 reactContext结合就能完成效果域的效果。并且进一步将store的创建合并到组件中,也能取得组件相关的生命周期。

想要在不同的store中彼此调用?能够

经过useBearStore.getState() 就能完成store间彼此调用。当然需求注意办理好store间的依靠联系。

想要中间件? 没问题

zustand 的库自带了一些中间件,其完成也十分简略。参考zustand/middleware的完成能够学习如何制作zustand的中间件。

想要处理异步action?没问题

redux前期,想要做异步action是十分头疼的工作,而rtk出来后会稍微好一点,可是也很费事。而在zustand,能够十分简略

const useFishStore = create((set) => ({
  fishies: {},
  fetch: async (pond) => {
    const response = await fetch(pond)
    set({ fishies: await response.json() })
  },
}))

缺乏与思考

再好的规划假如不加约束也会呈现 shit code。想要把 zustand 这样细巧而精美的库用好而不是用坏需求必定的技能办理能力。盲意图去运用新的技能并不必定能给技能团队带来一些收益,可是能够带来新的思考。

另一方面,zustand 是一种大局store的规划,不能说这种规划欠好,可是也意味着带来了一种比较经典的技能难题,即依靠办理。当项目中呈现彼此依靠的时分,如何办理,怎样保证在后续的保护中不构成污染,在调试时不会引进噪音。这是我认为所有的大局store都会面对的问题。