×

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

作者:Terry2022.02.28来源:Web前端之家浏览:4744评论:0
关键词:react

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

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

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

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

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

  1. import {
  2.   Dispatch,
  3.   SetStateAction,
  4.   useCallback,
  5.   useEffect,
  6.   useReducer,
  7.   useRef,
  8.   useState,
  9. } from 'react';
  10.  
  11. /**
  12.  * @see https://github.com/facebook/react/blob/bb88ce95a87934a655ef842af776c164391131ac/packages/shared/objectIs.js
  13.  * inlined Object.is polyfill to avoid requiring consumers ship their own
  14.  * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
  15.  */
  16. function is(x: any, y: any): boolean {
  17.   return (=== y && (!== 0 || 1 / x === 1 / y)) || (!== x && y !== y);
  18. }
  19.  
  20. const objectIs = typeof Object.is === 'function' ? Object.is : is;
  21.  
  22. /**
  23.  * @see https://github.com/facebook/react/blob/933880b4544a83ce54c8a47f348effe725a58843/packages/shared/shallowEqual.js
  24.  * Performs equality by iterating through keys on an object and returning false
  25.  * when any key has values which are not strictly equal between the arguments.
  26.  * Returns true when the values of all keys are strictly equal.
  27.  */
  28. function shallowEqual(objA: any, objB: any): boolean {
  29.   if (is(objA, objB)) {
  30.     return true;
  31.   }
  32.  
  33.   if (
  34.     typeof objA !== 'object' ||
  35.     objA === null ||
  36.     typeof objB !== 'object' ||
  37.     objB === null
  38.   ) {
  39.     return false;
  40.   }
  41.  
  42.   const keysA = Object.keys(objA);
  43.   const keysB = Object.keys(objB);
  44.  
  45.   if (keysA.length !== keysB.length) {
  46.     return false;
  47.   }
  48.  
  49.   // Test for A's keys different from B.
  50.   for (let i = 0; i < keysA.length; i++) {
  51.     if (
  52.       !Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
  53.       !is(objA[keysA[i]], objB[keysA[i]])
  54.     ) {
  55.       return false;
  56.     }
  57.   }
  58.  
  59.   return true;
  60. }
  61.  
  62. const useForceUpdate = () => useReducer(() => ({}), {})[1] as VoidFunction;
  63.  
  64. type ISubscriber<T> = (prevState: T, nextState: T) => void;
  65.  
  66. export interface ISharedState<T> {
  67.   /** 静态方式获取数据, 适合在非组件中或者数据无绑定视图的情况下使用 */
  68.   get: () => T;
  69.   /** 修改数据,赋予新值 */
  70.   set: Dispatch<SetStateAction<T>>;
  71.   /** (浅)合并更新数据 */
  72.   update: Dispatch<Partial<T>>;
  73.   /** hooks方式获取数据, 适合在组件中使用, 数据变更时会自动重渲染该组件 */
  74.   use: () => T;
  75.   /** 订阅数据的变更 */
  76.   subscribe: (cb: ISubscriber<T>) => () => void;
  77.   /** 取消订阅数据的变更 */
  78.   unsubscribe: (cb: ISubscriber<T>) => void;
  79.   /** 筛出部分 state */
  80.   usePick<R>(picker: (state: T) => R, deps?: readonly any[]): R;
  81. }
  82.  
  83. export type IReadonlyState<T> = Omit<ISharedState<T>, 'set' | 'update'>;
  84.  
  85. /**
  86.  * 创建不同实例之间可以共享的状态
  87.  * @param initialState 初始数据
  88.  */
  89. export const createSharedState = <T>(initialState: T): ISharedState<T> => {
  90.   let state = initialState;
  91.   const subscribers: ISubscriber<T>[] = [];
  92.  
  93.   // 订阅 state 的变化
  94.   const subscribe = (subscriber: ISubscriber<T>) => {
  95.     subscribers.push(subscriber);
  96.     return () => unsubscribe(subscriber);
  97.   };
  98.  
  99.   // 取消订阅 state 的变化
  100.   const unsubscribe = (subscriber: ISubscriber<T>) => {
  101.     const index = subscribers.indexOf(subscriber);
  102.     index > -1 && subscribers.splice(index, 1);
  103.   };
  104.  
  105.   // 获取当前最新的 state
  106.   const get = () => state;
  107.  
  108.   // 变更 state
  109.   const set = (next: SetStateAction<T>) => {
  110.     const prevState = state;
  111.     // @ts-ignore
  112.     const nextState = typeof next === 'function' ? next(prevState) : next;
  113.     if (objectIs(state, nextState)) {
  114.       return;
  115.     }
  116.     state = nextState;
  117.     subscribers.forEach((cb) => cb(prevState, state));
  118.   };
  119.  
  120.   // 获取当前最新的 state 的 hooks 用法
  121.   const use = () => {
  122.     const forceUpdate = useForceUpdate();
  123.  
  124.     useEffect(() => {
  125.       let isMounted = true;
  126.       // 组件挂载后立即更新一次, 避免无法使用到第一次更新数据
  127.       forceUpdate();
  128.       const un = subscribe(() => {
  129.         if (!isMounted) return;
  130.         forceUpdate();
  131.       });
  132.       return () => {
  133.         un();
  134.         isMounted = false;
  135.       };
  136.     }, []);
  137.  
  138.     return state;
  139.   };
  140.  
  141.   const usePick = <R>(picker: (s: T) => R, deps = []) => {
  142.     const ref = useRef<any>({});
  143.  
  144.     ref.current.picker = picker;
  145.  
  146.     const [pickedState, setPickedState] = useState<R>(() =>
  147.       ref.current.picker(state),
  148.     );
  149.  
  150.     ref.current.oldState = pickedState;
  151.  
  152.     const sub = useCallback(() => {
  153.       const pickedOld = ref.current.oldState;
  154.       const pickedNew = ref.current.picker(state);
  155.       if (!shallowEqual(pickedOld, pickedNew)) {
  156.         // 避免 pickedNew 是一个 function
  157.         setPickedState(() => pickedNew);
  158.       }
  159.     }, []);
  160.  
  161.     useEffect(() => {
  162.       const un = subscribe(sub);
  163.       return un;
  164.     }, []);
  165.  
  166.     useEffect(() => {
  167.       sub();
  168.     }, [...deps]);
  169.  
  170.     return pickedState;
  171.   };
  172.  
  173.   return {
  174.     get,
  175.     set,
  176.     update: (input: Partial<T>) => {
  177.       set((pre) => ({
  178.         ...pre,
  179.         ...input,
  180.       }));
  181.     },
  182.     use,
  183.     subscribe,
  184.     unsubscribe,
  185.     usePick,
  186.   };
  187. };

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

  1. // 创建一个状态实例
  2. const countState = createSharedState(0);
  3.  
  4. const A = () => {
  5.   // 在组件中使用 hooks 方式获取响应式数据
  6.   const count = countState.use();
  7.   return <div>A: {count}</div>;
  8. };
  9.  
  10. const B = () => {
  11.   // 使用 set 方法修改数据
  12.   return <button onClick={() => countState.set(count + 1)}>Add</button>;
  13. };
  14.  
  15. const C = () => {
  16.   return (
  17.     <button
  18.       onClick={() => {
  19.         // 使用 get 方法获取数据
  20.         console.log(countState.get());
  21.       }}
  22.     >
  23.       Get
  24.     </button>
  25.   );
  26. };
  27.  
  28. const App = () => {
  29.   return (
  30.     <>
  31.       </>
  32.       </>
  33.       </>
  34.     </>
  35.   );
  36. };

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

  1. const complexState = createSharedState({
  2.   a: 0,
  3.   b: {
  4.     c: 0,
  5.   },
  6. });
  7.  
  8. const A = () => {
  9.   const a = complexState.usePick((state) => state.a);
  10.   return <div>A: {a}</div>;
  11. };

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

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

  1. /**
  2.  * 状态派生(或 computed)
  3.  * ```ts
  4.  * const count1 = createSharedState(1);
  5.  * const count2 = createSharedState(2);
  6.  * const count3 = createDerivedState([count1, count2], ([n1, n2]) => n1 + n2);
  7.  * ```
  8.  * @param stores
  9.  * @param fn
  10.  * @param initialValue
  11.  * @returns
  12.  */
  13. export function createDerivedState<= any>(
  14.   stores: IReadonlyState<any>[],
  15.   fn: (values: any[]) => T,
  16.   opts?: {
  17.     /**
  18.      * 是否同步响应
  19.      * @default false
  20.      */
  21.     sync?: boolean;
  22.   },
  23. ): IReadonlyState<T> & {
  24.   stop: () => void;
  25. } {
  26.   const { sync } = { sync: false, ...opts };
  27.   let values: any[] = stores.map((it) => it.get());
  28.   const innerModel = createSharedState<T>(fn(values));
  29.  
  30.   let promise: Promise<void> | null = null;
  31.  
  32.   const uns = stores.map((it, i) => {
  33.     return it.subscribe((_old, newValue) => {
  34.       values[i] = newValue;
  35.  
  36.       if (sync) {
  37.         innerModel.set(() => fn(values));
  38.         return;
  39.       }
  40.  
  41.       // 异步更新
  42.       promise =
  43.         promise ||
  44.         Promise.resolve().then(() => {
  45.           innerModel.set(() => fn(values));
  46.           promise = null;
  47.         });
  48.     });
  49.   });
  50.  
  51.   return {
  52.     get: innerModel.get,
  53.     use: innerModel.use,
  54.     subscribe: innerModel.subscribe,
  55.     unsubscribe: innerModel.unsubscribe,
  56.     usePick: innerModel.usePick,
  57.     stop: () => {
  58.       uns.forEach((un) => un());
  59.     },
  60.   };
  61. }

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

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

使用感受

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

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

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

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

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

  1. const countState = createSharedState(0);
  2.  
  3. const App = () => {
  4.   useEffect(() => {
  5.     setInterval(() => {
  6.       console.log(countState.get());
  7.     }, 1000);
  8.   }, []);
  9.   // return ...
  10. };

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

  1. const countState = createSharedState(0);
  2.  
  3. const Content = () => {
  4.   const count = countState.use();
  5.   return <div>{count}</div>;
  6. };
  7.  
  8. const A = () => (
  9.   <button
  10.     onClick={() => {
  11.       Dialog.info({
  12.         title: 'Alert',
  13.         content: <Content />,
  14.       });
  15.     }}
  16.   >
  17.     open
  18.   </button>
  19. );

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

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

您的支持是我们创作的动力!
温馨提示:本文作者系Terry ,经Web前端之家编辑修改或补充,转载请注明出处和本文链接:
https://jiangweishan.com/article/react20220228a2.html

网友评论文明上网理性发言 已有0人参与

发表评论: