天天看點

如何利用 React Hooks 管理全局狀态

如何利用 React Hooks 管理全局狀态

React 社群最火的全局狀态管理庫必定是 Redux,但是 Redux 本身就是為了大型管理資料而妥協設計的——這就會讓一些小一點的應用一旦用上 Redux 就變得複雜無比。

後來又有了 Mobx,它對于小型應用的狀态管理确實比 Redux 簡單不少。可是不得不說 Mobx+React 簡直就是一個繁瑣版本的 Vue。是以我也不太喜歡,不如直接用 Vue3。

總而言之,不管是 react-redux 還是 mobx,他們使用的時候都非常複雜,甚至需要你去元件函數或是元件類上修修改改,從審美角度上來說就令人不太喜歡。

直到後來某一天用了 Angular,我就開始對 SOA 産生好感,ng 的 Service 的寫法與依賴注入控制反轉着實驚豔到了我。

Service 是 Angular 的邏輯複用方法,并且解決了共享狀态的問題,那 React 的自定義 Hook 可以達到類似的效果嘛?

可以,并且會比 Angular 更簡潔!!!

什麼是 Service​

我們先來想一下,Service 到底是什麼?

  • Service 包含 n 個方法;
  • Service 包含有狀态;
  • Service 應該是個單例。
  • 這些方法與狀态應該是高度整合的,一個 Service 解決的是一個子產品的問題。

例如下面這個負責 Todo List 記錄的 Service:

class TodoRecordService {
  private todoList: Record[] = [];


  get getTodoList() {
    return this.todoList;
  }


  public addRecord(newRecord: Record) {
    this.todoList.push(newRecord);
  }


  public deleteRecord(id: string) {
    this.todoList = this.todoList.filter((record) => record.id !== id);
  }


  public getRecord(id: string) {
    const targetIndex = this.todoList.findIndex((record) => record.id === id);
    return { index: targetIndex, ele: this.todoList[targetIndex] };
  }
}      

自定義 Service

那我們用 React 如何實作一個狀态共享的單例呢?

使用 Context 與 useContext 即可。

接下來我們做一個最簡單的計數器吧:一個負責計數的 button,一個負責顯示目前數值的 panel。

const App: React.FC = () => {
  return (
    <div>
      <Button />
      <Panel />
    </div>
  );
};      

然後我們來定義我們的 Service:

interface State {
  count: number;
  handleAdd: () => void;
}
export const CountService = createContext<State>(null);      

我們選擇讓一個 Context 成為一個 Service,這是因為我們可以利用 Context 的特性來進行狀态共享,達到單例的效果。

但是光這樣還不行,我們想讓 count 擁有響應性,就必須使用 useState(或者其他 hook)來建立。

是以需要一個自定義 Hook,并且在 Context.Provider 中傳入 Provider 的 value 值:

interface State {
  count: number;
  handleAdd: () => void;
}


export const CountService = createContext<State>(null);


export const useRootCountService = () => {
  const [count, setCount] = useState<number>(0);
  const handleAdd = useCallback(() => {
    setCount((n) => n + 1);
  }, []);


  return {
    count,
    handleAdd,
  };
};      

那麼在組建中,我們如何使用 Service 呢?

非常簡單:

const App: React.FC = () => {
  const countService = useContext(CountService);


  return <div>{countService.count}</div>;
};      

是以計數器的完整代碼應該這麼寫:

import { CountService, useRootCountService } from './service/count.service';


const App: React.FC = () => {
  return (
    <CountService.Provider value={useRooCountService()}>
      <div>
        <Button />
        <Panel />
      </div>
    </CountService.Provider>
  );
};


// Button.tsx
import { CountService } from '../services/global.service';


const Button: React.FC = () => {
  // 注意,此處是故意寫複雜了,是為了凸顯跨元件狀态管理的特性
  const countService = useContext(CountService);
  return <button onClick={() => countService.handleAdd()}>+</button>;
};


// Panel.tsx
import { CountService } from '../services/global.service';


const Panel: React.FC = () => {
  const countService = useContext(CountService);
  return <h2>{countService.count}</h2>;
};      

hooks 與 Service

對于小元件而言,剛剛的寫法已經足夠了。

但是要知道,Service 是高度集中的某個子產品的狀态與方法,我們不能保證 Service 的方法可以直接用到元件的邏輯中去。

是以需要我們在元件内部對于邏輯進行二次拼裝。

但是把邏輯直接寫到元件裡面是一件非常惡劣的事情!!!

幸好,React 有了 hooks 讓我們去抽離邏輯代碼。

const useLogic1 = () => {
  // 在 hook 中擷取服務
  const xxxService = useContext(XxxService);
  // ...
  const foo = useCallback(() => {
    // ...
    xxxService.xxxx();
    // ...
  }, []);


  return {
    // ...
    foo,
  };
};


const SomeComponent: React.FC = () => {
  // 複用邏輯
  const { a, b, foo } = useLogic1(someParams);
  const { c, bar } = useLogic2();


  return (
    <div>
      <button onClick={() => bar()}>Some Operation</button>
    </div>
  );
};      

這種形式的元件,便是我們的目标。

本文完〜