操作State

在Zustand中,对于State的操作都是通过Action来完成的,这与Flux所提倡的机制是一致的。Zustand中的Action与Redux和MobX中略微有些不同,其中最主要的不同就是Zustand中的Action在定义的时候限制更少。

在之前的实例中,已经出现过了Action的身影,在Store定义中,Action就是一个非常普通的方法。但是在之前的示例中,定义的Action函数式没有接受任何参数的,实际上,因为Zustand对于Action没有太多的限制,所以Action方法是可以接受任意形式和数量的参数的。

例如以下这个简单的示例。

const useGearBox = create(set => ({
  currentGear: 0,
  jumpShiftUp: amount => set(state => ({ currentGear: state.currentGear + amount }))
}));

除了可以在Action中接受参数以外,还可以定义异步Action。定义和使用异步Action在Redux中是不被允许的,因为Redux中的数据变化必须都要是明确的,但是因为Zustand中的数据变化是由set函数实现的,所以在何时变更状态,对于Zustand也是确定的事情。

Action中的异步可以用在任何位置上,甚至可以用在set函数内部,例如以下这个示例。

const useGearBox = create(set => ({
  currentGear: 0,
  ecoMode: false,
  switchEcoMode: async () => {
    const status = await fetchVehicleStatus();
    set(async state => ({ ecoMode: await switchEcoMode(status.engine) }));
  }
}));

在Action中同样也是可以访问当前Store中的状态的,但是必须要通过已鞥专门的函数来为Store生成一个快照,而不能直接访问Store中的状态。要在Action中访问当前Store中持有的状态,可以仿照以下示例进行。

const useGearBox = create((set, get) => ({
  currentGear: 0,
  shiftDown() {
    const current = get().currentGear;
    if (current > 0) {
      set(state => ({ currentGear: current - 1 }));
    }
  }
}));

从上面的示例中可以看到,不论是同步更改状态还是异步更改状态,Zustand都是通过create函数提供的set函数来实现的。其实set函数的使用远远不止上面示例中出现过的这些形式。在之前的示例中,传递给set函数的都是一个接受一个代表Store的参数的函数,对于状态的更新也是由这个函数来完成的。在默认情况下,set函数是采用合并的方法来更新状态的,也就是Store中所保存的状态只有在set函数中操作了的那一部分会发生变化,其余的状态都会维持原样。但是在一些情况下,我们可能会期望完全变更Store中持有的状态,这时就需要用到set函数的第二个参数了。set函数的第二个参数可以接受一个布尔类型的值,用于控制当前的set操作是采用拼合(Merge)还是替换(Replace),这个参数的默认值是false,就是采用拼合(Merge)来更新状态。

通过set函数的第二个参数,可以实现很多非常有用的用法。例如以下示例中所列举的操作。

import { omit } from 'ramda';

const useGearBox = create(set => ({
  currentGear: 0,
  ecoMode: false,
  sportMode: false,
  deleteSportMode: () => {
    set(state => omit(['sportMode'], state), true);
  },
  deleteEverything: () => {
    set({}, true);
  }
}));

在上面这个示例中,我们可以通过设置set函数的第二个参数为true来整体替换Store中持有的状态,但是需要注意的是,如示例中的deleteEverything这个Action所示,set函数所替换的内容不止是Store中保存的State,还包括了全部的Action。所以类似于示例中的操作实际上是会影响应用后续的运行的,在实际应用中并不应该使用。

示例中的deleteEverything这个Action原本的意图是重置Store中所保存的状态,但是因为set函数会简单的替换Store中的状态,所以如果要实现状态的重置,还需要一些额外的操作。以下示例所示的是如何正确的在Zustand中实现状态重置。

type GearBoxState = {
  currentGear: number;
  ecoMode: boolean;
  sportMode: boolean;
};

type GearBoxAction = {
  shiftUp: () => void;
  shiftDown: () => void;
  switchEcoMode: () => void;
  switchSportMode: () => void;
  reset: () => void;
};

const initialState: GearBoxState = {
  currentGear: 0,
  ecoMode: false,
  sportMode: false
};

const useGearBox = create<GearBoxState & GearBoxAction>(set => ({
  ...initialState,
  shiftUp: () => set(state => ({ currentGeat: state.currentGear + 1 })),
  shiftDown: () => set(state => ({ currentGeat: state.currentGear - 1 })),
  switchEcoMode: () => set(state => ({ ecoMode: !state.ecoMode })),
  switchSportMode: () => set(state => ({ sportMode: !state.sportMode })),
  reset: () => set({...initialState})
});

从上面这个示例中可以看出,其实要完成状态的重置,实际上最关键的步骤就是如何获取和保存Store中状态的原始状态,并且还要保证不会影响Store中的Action。上面示例中的这个实现方式通过分离定义的方式比较方便的实现了这个重置状态的需求。

但是这种重置的方法也会带来一定的模板代码,如果想要继续优化这些模板代码,那就需要自己重写一个create方法了。例如可以如同下面示例中一样,自动的为Store注入重置方法。

import actualCreate, { GetState, SetState, State, StoreApi, UseBoundStore } from 'zustand';

type WithRestter = {
  reset: () => void;
};

export const create: typeof actualCreate = <TState extends State>(
  createState: StateCreate<TState, SetState<TState>, GetState<TState>, any> | StoreApi<TState>
): UseBoundStore<TState & WithRestter, StoreApi<TState & WithRestter>> => {
  const slice = actualCreate(createState);
  const initialState = slice.getState();

  slice.reset = () => {
    slice.setState(initialState, true);
  };

  return slice;
};

利用上面这种重写create的方法,还可以很方便的实现同时重置应用中所有Store保存的状态。