自定义选择器

除了可以利用Store Hook从Store中精确地选择所需要的State和Action以外,直接使用没有任何参数形式的Hook还可以直接获取Store实例本身。例如以下示例中所示。

export const GearAssembly = () => {
  const store = useGearBox();

  return (
    <div>
      <div>Current Gear: {store.currentGear}</div>
      <button onClick={store.shiftUp}>Shift Up</button>
      <buton onClick={store.shiftDown}>Shift Down</buton>
    </div>
  );
};

这种用法虽然十分的方便,但是其缺点就是如果Store中持有的任何State发生了变化,那么组件就会发生重新渲染,这对于只是用到了Store中一部分内容的组件来说是十分不经济的。

Zustand生成的Hook还支持返回State的组合,但是如果使用Zustand的这项特性,就必须向Hook提供第二个参数。Zustand生成的Store Hook在使用的时候其第二个参数用来判断其所要返回的内容是否已经发生了变化。

例如一般可以从Store中组合返回以下形式的State。

export const GearAssembly = () => {
  // 从Store中以对象的形式组合返回两个State。
  // 当这两个State中的任何一个发生改变的时候,组件都会重新渲染。
  const { currentGear, ecoMode } = useGearBox(state => ({ state.currentGear, state.ecoMode }), shallow);

  // 从Store中以数组的形式组合返回两个State。
  // 与以对象的形式一样,组合中的任何一个对象的改变都会使组件重新渲染。
  const [gear, eco] = useGearBox(state => [state.currentGear, state.ecoMode], shallow);
};

组合返回State只是生成的Store Hook的一种用法,实际上这种用法并不是Zustand特意支持的,Zustand实际上支持的是对State进行变形,因为组合State也是State变形的一部分。例如还可以使用map等方法来使获取到的State先进行一个计算。

在上面的示例中,Store Hook的第二个参数都是使用了Zustand提供的shallow比较函数。Zustand提供的这个比较函数可以支持仅对Store Hook生成的对象中的第一层级内容进行比较,而不深入的比较各个字段内容的组成。除了使用Zustand提供的shallow函数以外,在使用过程中还可以自定义比较函数来提供Store Hook判定所获取到的内容是否发生了更改。自定义的比较函数需要接受两个参数,分别代表所返回值的上一个状态和当前状态的值,你需要根据Zustand提供的这两个值来提供一个表示值是否发生了变化的true或者false的返回值。

例如以下这个简单的判断。

export const StupidGear = () => {
  const currentGear = useGearBox(
    state => state.currentGear,
    (lastGear, newGear) => lastGear !== newGear
  );
};

自动生成选择器

从上面的使用和示例也许可以得出一个结论:每个组件中都要调用Store Hook反复选取其中的State产生的样板代码太多了,Zustand应该提供一种更加简单的方法来选取State。

Zustand提供了这样的方法,但是并没有集成在Zustand功能中,你可以使用Zustand提供的以下代码来创建一个工具函数。这个工具函数可以向Store Hook中注入自动生成的State选择器。

import { State, UseStore } from 'zustand';

// 这里定义自动生成的State选择的类型,所有自动生成的State选择器都将被注入到Store Hook中的use字段下。
interface Selectors<StoreType> {
  use: {
    [key in keyof StoreType]: () => StoreType[key];
  };
}

function createSelector<StoreType extends State>(store: UseStore<StoreType>) {
  (store as any).use = {};

  Object.keys(store.getState()).forEach(key => {
    const selector = (state: StoreType) => state[key as keyof StoreType];
    (store as any).use[key] = store(selector);
  };

  return store as UseStore<StoreType> & Selectors<StoreType>;
}

这样一来,使用这个工具函数就可以让我们用更加方便的方法选择所需要的State了,例如用这个工具函数改造一下之前的示例。

const useGearBoxBase = create({
  currentGear: 0,
  ecoMode: false,
  sportMode: false
});

const useGearBox = createSelectors(useGearBoxBase);

const GearAssembly = () => {
  // 之前使用Store Hook选取State的写法,现在就变成了一个预置的函数。
  const gear = useGearBox.use.currentGear();

  // ...
};

除了可以自定义这个用于自动生成State选择器的工具函数以外,还可以通过安装auto-zustand-selectors-hook工具库,来使用其中提供的createSelectorFunctions工具函数来实现相同的功能。

监控状态变化

在使用状态存储的时候,往往会出现监控某一个或者某几个状态的变化然后自动执行对应的方法的需求。在Zustand中,这种需求可以借助subscribeWithSelector中间件来实现。subscribeWithSelector中间件是Zustand内置的中间件,在定义的时候直接使用即可。

例如以下示例中所示。

import create from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';
import shallow from 'zustand/shallow';

// 需要使用监控功能的Store,在定义的时候需要需要特别指出所需要使用的中间件。
const useGearBox = create(
  subscribeWithSelector(set => ({
    currentGear: 0,
    ecoMode: false,
    sportMode: false,
    shiftUp: () => set(state => ({ currentGear: state.currentGear + 1 })),
    shiftDown: () => set(state => ({ currentGear: state.currentGear - 1 })),
    toggleEcoMode: () => set(state => ({ ecoMode: !state.ecoMode })),
    toggleSportMode: () => set(state => ({ sportMode: !state.sportMode }))
  }))
);

// subscribe的第一个参数是选取所要监控的状态。
const unsubGear = useGearBox.subscribe(
  state => state.currentGear,
  gear => console.log(`Current Gear: ${gear}`)
);

// subscribe的第二个参数所接收的函数实际上是可以接受两个参数的,
// 其中第二个参数是所选择状态在发生变化时之前的旧值。
const unsubGearHistory = useGearBox.subscribe(
  state => state.currentGear,
  (gear, previousGear) => console.log(`Shift from ${previousGear} to ${gear}`)
);

// 同样subscribe也可以同时监控多个状态,但是此时需要使用subscribe的第三个参数来配置所监控内容发生变化的条件。
const unsubMode = useGearBox.subscribe(
  state => [state.ecoMode, state.sportMode],
  ([ecoMode, sportMode]) => console.log(`Eco Mode: ${ecoMode}, Sport Mode: ${sportMode}`),
  { equalityFn: shallow }
);

// subscribe默认情况下是只在状态发生变化的时候才会触发,在subscribe的第三个参数中配置fireImmediately可以在监控创建的时候立刻触发。
const ubsubImmediately = useGearBox.subscribe(state => state.currentGear, console.log, { fireImmediately: true });

引入subscribeWithSelector中间件以后,就给创建的Store Hook中增加了一个subscribe方法,允许通过这个方法来在Store中的状态发生变化的时候执行一些响应操作。

在监控被创建的时候,会返回一个用于注销监控的方法,如上面示例中返回的unsub开头的函数。这些用于注销的函数在实际使用中是非常有用的,可以避免内存泄漏的情况出现。

在监控的响应函数中同样可以对Store中的状态进行更改,要完成这个操作只需要调用Store中的Action即可。但是需要注意的是,在响应函数中一定不要修改当前被监控的状态,否则将引起循环触发造成死循环出现。

瞬态更新

Store中所保存的状态在更新的时候通常是会引起React组件重新渲染的,但是有一些时候我们可能并不希望组件响应状态的更新。在这种需求下,可以采用类似于以下示例中的用法来获取Store中的状态,但不用重新渲染组件。

const TransientGearBox = () => {
  const gearRef = useRef(useGearBox.getState().scratches);

  useEffect(() => {
    const unsub = useGearBox.subscribe(state => (gearRef.current = state.scratches));

    return () => unsub();
  }, []);

  // 组件剩余部分。
};

如同示例中所使用的方法,在组件中创建一个Store的瞬态引用,可以避免在Store中的状态发生变化的时候使React重新渲染组件,因为到Store瞬态的引用是始终保持不变的。组件中所需要做的就是在useEffect中为这个引用赋予实际的内容。