CAFECA-IO / iSunFA

Artificial Intelligence in Financial
https://isunfa.com
GNU General Public License v3.0
0 stars 0 forks source link

[Fix] APIHandler #3291

Closed godmmt closed 1 day ago

godmmt commented 2 days ago

APIHandler 會引發 Error: Invalid hook call

並且他回傳的 trigger 函數是不能放在 useEffect 的依賴項陣列裡面 會瘋狂的觸發 useEffect

檔案位置:

src/lib/utils/api_handler.ts
godmmt commented 2 days ago

確認問題

src/lib/utils/api_handler.ts

點擊展開

```ts import { APIConfig } from '@/constants/api_connection'; import { IAPIName, IAPIConfig, IAPIInput, IAPIResponse } from '@/interfaces/api_connection'; import useAPIWorker from '@/lib/hooks/use_api_worker'; import useAPI from '@/lib/hooks/use_api'; import eventManager from '@/lib/utils/event_manager'; import { STATUS_CODE, STATUS_MESSAGE } from '@/constants/status_code'; function checkInput(apiConfig: IAPIConfig, input?: IAPIInput) { // TODO: (20240504 - Luphia) check if params match the input schema if (!input) { throw new Error('Input is required'); } return true; } function APIHandler( apiName: IAPIName, options: IAPIInput = { header: {}, body: {}, params: {}, query: {}, }, triggerImmediately: boolean = false, cancel: boolean = false ): IAPIResponse { const apiConfig = APIConfig[apiName]; if (!apiConfig) throw new Error(`API ${apiName} is not defined`); checkInput(apiConfig, options); let response = {} as IAPIResponse; if (apiConfig.useWorker) { response = useAPIWorker(apiConfig, options, triggerImmediately, cancel); } else response = useAPI(apiConfig, options, triggerImmediately, cancel); const { code } = response; if (code === STATUS_CODE[STATUS_MESSAGE.UNAUTHORIZED_ACCESS]) { eventManager.emit(STATUS_MESSAGE.UNAUTHORIZED_ACCESS); } return response; } export default APIHandler; ```

打開 eslint 檢查

image https://github.com/user-attachments/assets/64329799-8b6c-4857-8ff5-47cb52c9eb42

並且注意到

APIHandler 會引發 Error: Invalid hook call 的原因可能是因為

不能在 useEffect 裡面寫 const { trigger: listUserCompanyAPI } = APIHandler<ICompanyAndRole[]>(APIName.LIST_USER_COMPANY);

以及

就算在元件中先取得 trigger 函數 但把 trigger 放入 useEffect 中使用的話 無法正確放入依賴項 (遵循 Eslint 規則) 如果放入 會瘋狂觸發 useEffect

godmmt commented 2 days ago

使用 useEffect 呼叫 APIHandler 遇到的相關問題

問題

  1. 不能在 useEffect 直接呼叫 APIHandler
  2. 改成在元件中呼叫 APIHandler 並且取得的 trigger 再放入 useEffect 裡使用,卻不能放入 useEffect 作為依賴項(無法遵循 Eslint 建議),會瘋狂觸發 useEffect

研究

問題可能來自於在 APIHandler 中直接呼叫 useAPIuseAPIWorker,但 APIHandler 本身既不是 React 元件,也不是自訂 Hook。

這違反了 Hooks 規則,因為 React Hook 僅能在以下兩種情境中被呼叫:

  1. React 函數元件的頂層。
  2. 另一個自訂 Hook 內。

APIHandler 函數中,React 無法辨識其為有效的上下文,直接呼叫像 useAPI 這樣的 Hook 會導致「Invalid hook call」錯誤。

godmmt commented 2 days ago

解決方式有兩種

方法 1:將 APIHandler 保留為一般函式

然後確保內部呼叫的 Hook(useAPIuseAPIWorker)遵守 Hooks 規則。

優點:

  1. 簡潔性:你可以直接以同步的方式呼叫 APIHandler,回傳的 response 就是一個標準的物件,不需要使用 Hook 的規範(例如 useEffectuseState)。
  2. 適用範圍廣:如果某些情況下 APIHandler 不需要在 React 元件內使用,這種方式更靈活。
  3. 低影響範圍:保持現有的 APIHandler 結構,減少對現有代碼的改動。

缺點:

  1. 潛在性能損失:每次都執行 useAPIuseAPIWorker,即使其中一個結果沒有被用到,會浪費資源。
  2. React 元件外不可用 Hook:如果你的程式需要完全依賴 Hooks 的邏輯,這種方式的靈活性可能反而成為限制。
重構程式碼 (點擊展開)

```ts function APIHandler( apiName: IAPIName, options: IAPIInput = { header: {}, body: {}, params: {}, query: {}, }, triggerImmediately: boolean = false, cancel: boolean = false ): IAPIResponse { const apiConfig = APIConfig[apiName]; if (!apiConfig) throw new Error(`API ${apiName} is not defined`); checkInput(apiConfig, options); // 呼叫兩個 Hook,確保它們的執行順序一致 const workerResponse = useAPIWorker(apiConfig, options, triggerImmediately, cancel); const apiResponse = useAPI(apiConfig, options, triggerImmediately, cancel); // 根據條件決定使用哪一個結果 const response = apiConfig.useWorker ? workerResponse : apiResponse; const { code } = response; if (code === STATUS_CODE[STATUS_MESSAGE.UNAUTHORIZED_ACCESS]) { eventManager.emit(STATUS_MESSAGE.UNAUTHORIZED_ACCESS); } return response; } ```

方法 2:將 APIHandler 改為自定義 Hook(useAPIHandler)

這種方式是將 APIHandler 本身改為一個 Hook,並直接遵循 React Hooks 的設計模式。

優點:

  1. 符合 React 思維:所有邏輯與狀態管理都使用 Hooks,使程式風格更加一致。
  2. 避免性能浪費:可以根據 apiConfig.useWorker 條件只執行對應的 Hook,提升性能。
  3. 靈活使用 Hooks:自定義 Hook 可以直接用於 React 元件中,便於統一管理狀態與副作用。

缺點:

  1. 適用性受限:只能在 React 元件或其他 Hook 中使用,不能用於 React 範圍之外的邏輯。
  2. 需要改動範圍大:會影響現有調用 APIHandler 的地方,需要改為遵循 Hooks 的使用方式。
重構程式碼 (點擊展開)

```ts import { APIConfig } from '@/constants/api_connection'; import { IAPIName, IAPIConfig, IAPIInput, IAPIResponse } from '@/interfaces/api_connection'; import useAPIWorker from '@/lib/hooks/use_api_worker'; import useAPI from '@/lib/hooks/use_api'; import eventManager from '@/lib/utils/event_manager'; import { STATUS_CODE, STATUS_MESSAGE } from '@/constants/status_code'; function checkInput(apiConfig: IAPIConfig, input?: IAPIInput) { // TODO: (20240504 - Luphia) check if params match the input schema if (!input) { throw new Error('Input is required'); } return true; } const useAPIHandler = ( apiName: IAPIName, options: IAPIInput = { header: {}, body: {}, params: {}, query: {}, }, triggerImmediately: boolean = false, cancel: boolean = false ): IAPIResponse => { const apiConfig = APIConfig[apiName]; if (!apiConfig) throw new Error(`API ${apiName} is not defined`); checkInput(apiConfig, options); // Choose the appropriate hook based on `useWorker` const response = apiConfig.useWorker ? useAPIWorker(apiConfig, options, triggerImmediately, cancel) : useAPI(apiConfig, options, triggerImmediately, cancel); // Handle unauthorized access event if (response.code === STATUS_CODE[STATUS_MESSAGE.UNAUTHORIZED_ACCESS]) { eventManager.emit(STATUS_MESSAGE.UNAUTHORIZED_ACCESS); } return response; }; export default useAPIHandler; ```

godmmt commented 2 days ago

哪種方式更好?

選擇方法 1(一般函式):

選擇方法 2(自定義 Hook):

最佳實踐建議

  1. 短期目標: 如果需要快速解決問題,且目前的設計滿足需求,保持 APIHandler 作為一般函式即可。

  2. 長期目標: 如果將來項目更偏向 React Hooks 的設計模式(例如統一使用自定義 Hook 管理邏輯),考慮將 APIHandler 改為 useAPIHandler 是更好的策略。

godmmt commented 2 days ago

結論

已經確認過我們使用 APIHandler 的地方都在元件或 context 內 (Hook) ✅ 所以我們是可以採用方法 2

但程式碼的變動範圍會比較大 目前時程比較趕 所以決定先用方法 1 暫時解決這個錯誤

等之後可以回頭改善程式碼品質的時候 再一次採用方法 2 來修改

截圖 2024-11-20 16 29 36

https://github.com/user-attachments/assets/813a9aab-6299-4ea1-a060-1377d26ad067

注意

用方法 1 還是無法避免一個問題

就是 APIHandler 回傳的 trigger 函數是不能放在 useEffect 的依賴項陣列裡面 會瘋狂的觸發

也許改成自訂 Hook 後 trigger 也可放在 useEffect 的依賴項陣列裡面、 或者甚至直接在 useEffect 內執行 APIHandler 並拿到 trigger 這樣甚至不用把 trigger 放在依賴項

const { trigger: listUserCompanyAPI } = APIHandler<ICompanyAndRole[]>(APIName.LIST_USER_COMPANY); 不能寫在 useEffect 裡面

godmmt commented 2 days ago

took 3 hours

done