1. 程式人生 > 程式設計 >基於React Hooks的小型狀態管理詳解

基於React Hooks的小型狀態管理詳解

目錄
  • 實現基於 React Hooks 的狀態共享
  • 使用感受

本文主要介紹一種基於 React Hooks 的狀態共享方案,介紹其實現,並總結一下使用感受,目的是在狀態管理方面提供多一種選擇方式。

實現基於 React Hooks 的狀態共享

React 元件間的狀態共享,是一個老生常談的問題,也有很多解決方案,例如 Redux、MobX 等。這些方案很專業,也經歷了時間的考驗,但私以為他們不太適合一些不算複雜的專案,反而會引入一些額外的複雜度。

實際上很多時候,我不想定義 mutation 和 action、我不想套一層 context,更不想寫 connect 和 mapStateToProps;我想要的是一種輕量、簡單的狀態共享方案,簡簡單單引用、簡簡單單使用。

隨著 Hooks 的誕生、流行,我的想法得以如願。

接著介紹一下我目前在用的方案,將 Hooks 與釋出/訂閱模式結合,就能實現一種簡單、實用的狀態共享方案。因為程式碼不多,下面將給出完整的實現。

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

/**
 * @see https://.com/facebook/react/blob/bb88ce95a87934a655ef842af776c164391131ac/packages/shared/objectIs.
 * inlined Object.is polyfill to avoid requiring consumers ship their own
 * https://developer.mozilla.org/en-US/docs/Web//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 a
ny 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>http://www.cppcns.com
{ /** 靜態方式獲取資料,適合在非元件中或者資料無繫結檢視的情況下使用 */ 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,nexwww.cppcns.comtState)) { 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:RqzfI {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 專案中均能使用同一套實現,一直都比較順利。

使用感受

最後總結一下目前這種方式的幾個特點:

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 應用程式碼

以上,便是本文的全部內容,實際上 Hooks 到目前流行了這麼久,社群當中已有不少新型的狀態共享實現方式,這裡僅作為一種參考。http://www.cppcns.com

根據以上特點,這種方式有明顯的優點,也有致命的缺陷(對於 SSR 而言),但在實際使用中,可以根據具體的情況來選擇合適的方式。比如在 Taro2 的小程式應用中,無需關心 SSR,那麼我更傾向於這種方式;如果在 SSR 的同構專案中,那麼定還是老老實實選擇 Redux。

總之,是多了一種選擇,到底怎麼用還得視具體情況而定。

以上就是基於React Hooks的小型狀態管理詳解的詳細內容,更多關於React Hooks 小型狀態管理的資料請關注我們其它相關文章!