用 reaction hook 讓副作用各自帶依賴
主張:要解掉 共用能力 context 會隨副作用種類膨脹,把每個反應做成自帶依賴的 reaction hook,再抽一個共用樣板 hook 收掉去重/守衛的重複邏輯。「拿依賴」的責任從中央 dispatcher 還給各家,共用 ctx 就不必再當聯集。
骨架
① 共用樣板——不認得任何 concrete workflow,只留 seam:
// useClosedJobReaction.ts
export const useClosedJobReaction = (
projectId: string | undefined,
opts: {
workflowTypes: string[]; // 支援多 type 共用(bulk 兩個 workflow)
shouldHandle: (job: InFlightJobItem) => boolean;
onHandle: (job: InFlightJobItem) => void;
}
) => {
const { data } = useProjectInFlightJobs(projectId);
useEffect(() => {
data?.closedJobs?.forEach((job) => {
if (!opts.workflowTypes.includes(job.workflowType) || !opts.shouldHandle(job)) return;
if (hasHandledJob(job.runId)) return;
markJobHandled(job.runId); // ★ 先 mark 再 run,仍擋 re-entrant
opts.onHandle(job);
});
}, [data?.closedJobs, projectId]);
};② 每個反應只拿自己要的依賴——沒有聯集、沒有共用 ctx:
export const useTransportationSegmentsReaction = (projectId?: string) => {
const { showToast, closeToast } = useNewTransportationSegmentsToast(); // 只 import 自己要的
const shown = useRef<Set<string>>(new Set());
useClosedJobReaction(projectId, {
workflowTypes: [TRANSPORTATION_AGGREGATE_WORKFLOW],
shouldHandle: (j) => j.status === COMPLETED && j.hasNewTransportationSegments === true,
onHandle: (j) => { showToast(j.runId); shown.current.add(j.runId); },
});
useEffect(() => () => { // 切換專案關 toast 的政策,留在這個反應自己身上
shown.current.forEach(closeToast); shown.current.clear();
}, [projectId, closeToast]);
};③ 掛載點是一份清單:
useTransportationSegmentsReaction(projectId);
useBulkCreatedReaction(projectId);
useXxxApiReaction(projectId); // 打 API 的新 workflow:內部自己 useXxxApi(),別人不受影響為什麼不再膨脹
| 共用 ctx | reaction hook | |
|---|---|---|
| 依賴誰拿 | dispatcher 全拿、塞進聯集 | 每個反應只拿自己要的 |
| 介面 | 一個聯集介面(違反 ISP) | 沒有共用介面,天生隔離 |
| 加一種副作用 | 撐大 ctx + dispatcher | 新增一檔、掛一行,既有不動 |
| 中央耦合 | dispatcher 認得全部 | 沒有中央點 |
去重、job 來源、樣板邏輯仍共用(useClosedJobReaction + closedJobHandledStorage),不是退回重複程式碼——只是把「拿依賴」從中央搬回各家。
附帶好處:消掉「框架內 vs 框架外」的分裂
原本 useAggregationCompletionFactorToast 因為要 async diff 被刻意留在框架外。改成這個模型後全部都是 reaction hook:單純的用共用樣板、複雜/async 的就不用樣板自己寫 effect,那條人為界線消失。
取捨
- 失去
CLOSED_JOB_HANDLERS那張「一眼看完所有反應」的中央目錄,換成掛載點一份 hook 清單。 - 掛載點一種反應一行——但這誠實反映「你就是在 mount N 個獨立反應」。
- 若很想留中央目錄:可把註冊表改成
Record<type, () => Runtime>(值是 hook),用Object.values(REGISTRY).forEach(useIt)迭代;registry 是 module 常數、順序固定不違反 rules of hooks,但 ESLint hooks plugin 會叫、也較 magic。