51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

React:基于React Hooks的小型状态管理

React:基于React Hooks的小型状态管理。

React 组件间的状态共享,是一个老生常谈的问题,也有很多解决方案,例如 Redux、MobX 等。这些方案很专业,也经历了时间的考验,但私以为他们不太适合一些不算复杂的项目,反而会引入一些额外的复杂度。

实际上很多时候,我不想定义 mutation 和 action、我不想套一层 context,更不想写 connect 和 mapStateToProps;我想要的是一种轻量、简单的状态共享方案,简简单单引用、简简单单使用。

随着 Hooks 的诞生、流行,我的想法得以如愿。

接着介绍一下我目前在用的方案,将 Hooks 与发布/订阅模式结合,就能实现一种简单、实用的状态共享方案。因为代码不多,下面将给出完整的实现。

import {
  Dispatch,
  SetStateAction,
  useCallback,
  useEffect,
  useReducer,
  useRef,
  useState,
} from 'react';

/**  * @see https://github.com/facebook/react/blob/bb88ce95a87934a655ef842af776c164391131ac/packages/shared/objectIs.js  * inlined Object.is polyfill to avoid requiring consumers ship their own  * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is  */ function is(x: any, y: any): boolean {   return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y); }

const objectIs = typeof Object.is === 'function' ? Object.is : is;

/**  * @see https://github.com/facebook/react/blob/933880b4544a83ce54c8a47f348effe725a58843/packages/shared/shallowEqual.js  * Performs equality by iterating through keys on an object and returning false  * when any key has values which are not strictly equal between the arguments.  * Returns true when the values of all keys are strictly equal.  */ function shallowEqual(objA: any, objB: any): boolean {   if (is(objA, objB)) {     return true;   }

  if (     typeof objA !== 'object' ||     objA === null ||     typeof objB !== 'object' ||     objB === null   ) {     return false;   }

  const keysA = Object.keys(objA);   const keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {     return false;   }

  // Test for A's keys different from B.   for (let i = 0; i < keysA.length; i++) {     if (       !Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||       !is(objA[keysA[i]], objB[keysA[i]])     ) {       return false;     }   }

  return true; }

const useForceUpdate = () => useReducer(() => ({}), {})[1] as VoidFunction;

type ISubscriber<T> = (prevState: T, nextState: T) => void;

export interface ISharedState<T> {   /** 静态方式获取数据, 适合在非组件中或者数据无绑定视图的情况下使用 /   get: () => T;   /* 修改数据,赋予新值 /   set: Dispatch<SetStateAction<T>>;   /* (浅)合并更新数据 /   update: Dispatch<Partial<T>>;   /* hooks方式获取数据, 适合在组件中使用, 数据变更时会自动重渲染该组件 /   use: () => T;   /* 订阅数据的变更 /   subscribe: (cb: ISubscriber<T>) => () => void;   /* 取消订阅数据的变更 /   unsubscribe: (cb: ISubscriber<T>) => void;   /* 筛出部分 state */   usePick<R>(picker: (state: T) => R, deps?: readonly any[]): R; }

export type IReadonlyState<T> = Omit<ISharedState<T>, 'set' | 'update'>;

/**  * 创建不同实例之间可以共享的状态  * @param initialState 初始数据  */ export const createSharedState = <T>(initialState: T): ISharedState<T> => {   let state = initialState;   const subscribers: ISubscriber<T>[] = [];

  // 订阅 state 的变化   const subscribe = (subscriber: ISubscriber<T>) => {     subscribers.push(subscriber);     return () => unsubscribe(subscriber);   };

  // 取消订阅 state 的变化   const unsubscribe = (subscriber: ISubscriber<T>) => {     const index = subscribers.indexOf(subscriber);     index > -1 && subscribers.splice(index, 1);   };

  // 获取当前最新的 state   const get = () => state;

  // 变更 state   const set = (next: SetStateAction<T>) => {     const prevState = state;     // @ts-ignore     const nextState = typeof next === 'function' ? next(prevState) : next;     if (objectIs(state, nextState)) {       return;     }     state = nextState;     subscribers.forEach((cb) => cb(prevState, state));   };

  // 获取当前最新的 state 的 hooks 用法   const use = () => {     const forceUpdate = useForceUpdate();

    useEffect(() => {       let isMounted = true;       // 组件挂载后立即更新一次, 避免无法使用到第一次更新数据       forceUpdate();       const un = subscribe(() => {         if (!isMounted) return;         forceUpdate();       });       return () => {         un();         isMounted = false;       };     }, []);

    return state;   };

  const usePick = <R>(picker: (s: T) => R, deps = []) => {     const ref = useRef<any>({});

    ref.current.picker = picker;

    const [pickedState, setPickedState] = useState<R>(() =>       ref.current.picker(state),     );

    ref.current.oldState = pickedState;

    const sub = useCallback(() => {       const pickedOld = ref.current.oldState;       const pickedNew = ref.current.picker(state);       if (!shallowEqual(pickedOld, pickedNew)) {         // 避免 pickedNew 是一个 function         setPickedState(() => pickedNew);       }     }, []);

    useEffect(() => {       const un = subscribe(sub);       return un;     }, []);

    useEffect(() => {       sub();     }, [...deps]);

    return pickedState;   };

  return {     get,     set,     update: (input: Partial<T>) => {       set((pre) => ({         ...pre,         ...input,       }));     },     use,     subscribe,     unsubscribe,     usePick,   }; };

拥有 createSharedState 之后,下一步就能轻易地创建出一个可共享的状态了,在组件中使用的方式也很直接。

// 创建一个状态实例
const countState = createSharedState(0);

const A = () => {   // 在组件中使用 hooks 方式获取响应式数据   const count = countState.use();   return <div>A: {count}</div>; };

const B = () => {   // 使用 set 方法修改数据   return <button onClick={() => countState.set(count + 1)}>Add</button>; };

const C = () => {   return (     <button       onClick={() => {         // 使用 get 方法获取数据         console.log(countState.get());       }}     >       Get     </button>   ); };

const App = () => {   return (     <>       <A />       <B />       <C />     </>   ); };

对于复杂对象,还提供了一种方式,用于在组件中监听指定部分的数据变化,避免其他字段变更造成多余的 render:

const complexState = createSharedState({
  a: 0,
  b: {
    c: 0,
  },
});

const A = () => {   const a = complexState.usePick((state) => state.a);   return <div>A: {a}</div>; };

但复杂对象一般更建议使用组合派生的方式,由多个简单的状态派生出一个复杂的对象。另外在有些时候,我们会需要一种基于原数据的计算结果,所以这里同时提供了一种派生数据的方式。

通过显示声明依赖的方式监听数据源,再传入计算函数,那么就能得到一个响应式的派生结果了。

/**
 * 状态派生(或 computed)
 * ```ts
 * const count1 = createSharedState(1);
 * const count2 = createSharedState(2);
 * const count3 = createDerivedState([count1, count2], ([n1, n2]) => n1 + n2);
 * ```
 * @param stores
 * @param fn
 * @param initialValue
 * @returns
 */
export function createDerivedState<T = any>(
  stores: IReadonlyState<any>[],
  fn: (values: any[]) => T,
  opts?: {
    /**
     * 是否同步响应
     * @default false
     */
    sync?: boolean;
  },
): IReadonlyState<T> & {
  stop: () => void;
} {
  const { sync } = { sync: false, ...opts };
  let values: any[] = stores.map((it) => it.get());
  const innerModel = createSharedState<T>(fn(values));

  let promise: Promise<void> | null = null;

  const uns = stores.map((it, i) => {     return it.subscribe((_old, newValue) => {       values[i] = newValue;

      if (sync) {         innerModel.set(() => fn(values));         return;       }

      // 异步更新       promise =         promise ||         Promise.resolve().then(() => {           innerModel.set(() => fn(values));           promise = null;         });     });   });

  return {     get: innerModel.get,     use: innerModel.use,     subscribe: innerModel.subscribe,     unsubscribe: innerModel.unsubscribe,     usePick: innerModel.usePick,     stop: () => {       uns.forEach((un) => un());     },   }; }

至此,基于 Hooks 的状态共享方的实现介绍就结束了。

在最近的项目中,有需要状态共享的场景,我都选择了上述方式,在 Web 项目和小程序 Taro 项目中均能使用同一套实现,一直都比较顺利。

{#_label1}

使用感受 {#heading-1}

最后总结一下目前这种方式的几个特点:

1.实现简单,不引入其他概念,仅在 Hooks 的基础上结合发布/订阅模式,类 React 的场景都能使用,比如 Taro;

2.使用简单,因为没有其他概念,直接调用 create 方法即可得到 state 的引用,调用 state 实例上的 use 方法即完成了组件和数据的绑定;

3.类型友好,创建 state 时无需定义多余的类型,使用的时候也能较好地自动推导出类型;

4.避免了 Hooks 的"闭包陷阱",因为 state 的引用是恒定的,通过 state 的 get 方法总是能获取到最新的值:

const countState = createSharedState(0);

const App = () => {   useEffect(() => {     setInterval(() => {       console.log(countState.get());     }, 1000);   }, []);   // return ... };

5.直接支持在多个 React 应用之间共享,在使用一些弹框的时候是比较容易出现多个 React 应用的场景:

const countState = createSharedState(0);

const Content = () => {   const count = countState.use();   return <div>{count}</div>; };

const A = () => (   <button     onClick={() => {       Dialog.info({         title: 'Alert',         content: <Content />,       });     }}   >     open   </button> );

6.支持在组件外的场景获取/更新数据。

7.在 SSR 的场景有较大局限性:state 是细碎、分散创建的,而且 state 的生命周期不是跟随 React 应用,导致无法用同构的方式编写 SSR 应用代码。

赞(2)
未经允许不得转载:工具盒子 » React:基于React Hooks的小型状态管理