
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>
);
};
這種形式的元件,便是我們的目标。
本文完〜