紀錄工作經驗、相關知識,解決技術相關問題。

Javascript, React, 工作雜談

React useEffect 異常診斷與修復:TypeError: destroy is not a function 報錯分析與非同步競態處理實務

React useEffect 異常診斷與修復:TypeError: destroy is not a function 報錯分析與非同步競態處理實務 特色圖片

React 報錯解決:useEffect 出現 TypeError: destroy is not a function 原因與 async/await 正確寫法

React useEffect 直接傳入 async 函式觸發 TypeError: destroy is not a function 錯誤畫面

在 React 或 Next.js 開發過程中,當你在 useEffect Hook 中嘗試直接使用 async/await 語法時,瀏覽器開發者工具常會彈出 Unhandled Runtime Error 或 TypeError: destroy is not a function(在 Next.js 中常顯示為 n of n unhandled error)。這通常是因為對 useEffect 的回傳機制(Cleanup Function)與 JavaScript async 函式的回傳值產生誤解所致。

本文將解析此錯誤的根本原因,並提供符合生產環境規範的解決思路。

常見問題(Q&A)與程式碼範本快速跳轉


核心機制對照表:useEffect 預期值 vs. Async 函式

維度useEffect 回傳要求async 函式回傳值衝突點
資料型別必須是 function (清理函式) 或 undefined永遠回傳一個 Promise 物件React 預期收到函式,卻收到 Promise
執行時機元件卸載 (Unmount) 或依賴項改變前呼叫後立即回傳 PromiseReact 試圖執行 Promise 物件導致報錯
報錯訊息無TypeError: destroy is not a functionPromise 物件沒有執行能力

1. 錯誤原因解析:為什麼會報錯?

在 React 的設計中,useEffect 的第一個參數(Effect Callback)只允許回傳兩種東西:

  1. undefined(不回傳任何內容)。
  2. 一個函式(稱為 Cleanup Function 或 destroy 函式),用於清除副作用(如 setInterval 或訂閱事件)。

當你將 useEffect 直接標記為 async 時:

// ❌ 錯誤範例:直接將 useEffect 設為 async
useEffect(async () => {
  const data = await fetchData();
}, []);

根據 JavaScript 規範,async 函式保證會回傳一個 Promise。對 React 來說,它收到了這個 Promise 並將其視為「清理函式(Cleanup Function)」。當元件準備卸載或重新執行 Effect 時,React 會嘗試呼叫這個 Promise(內部變數名為 destroy),但因為 Promise 不是函式,所以會拋出 TypeError: destroy is not a function。


2. 解決思路與程式碼範本

解法一:在 useEffect 內部宣告 async 函式並處理 Race Condition(推薦)

這是最標準且安全的做法。保持 useEffect 本身是同步函式,但在內部定義並執行一個非同步函式。同時,為了防止元件快速切換導致的 Race Condition(競態條件),建議搭配 AbortController 來取消過時的 API 請求。

useEffect(() => {
  // 1. 宣告 AbortController 用於取消請求
  const controller = new AbortController();
  const signal = controller.signal;

  const fetchAndSetData = async () => {
    try {
      const response = await fetch('https://api.example.com/data', { signal });
      const data = await response.json();
      
      // 處理資料邏輯
      console.log(data);
    } catch (error) {
      if (error.name === 'AbortError') {
        console.log('Request was cancelled');
      } else {
        console.error('API Error:', error);
      }
    }
  };

  // 2. 執行非同步函式
  fetchAndSetData();

  // 3. Cleanup Function:在元件卸載或依賴項改變時執行
  return () => {
    // 取消尚未完成的請求,避免 Race Condition
    controller.abort();
  };
}, []);

解法二:使用 IIFE (立即執行函式)

如果你不想在內部額外命名函式,可以使用 IIFE (Immediately Invoked Function Expression)。這種寫法較為簡潔,但可讀性略低於解法一。

useEffect(() => {
  (async () => {
    try {
      const data = await getApiData();
      // 處理資料
    } catch (error) {
      console.error(error);
    }
  })();

  return () => {
    // Cleanup 邏輯
  };
}, []);

解法三:將非同步邏輯抽離至外部函式

當 API 邏輯較為複雜時,建議將其封裝在 useEffect 之外,保持 Hook 內部的簡潔。

// 獨立的非同步函式
const getApiData = async () => {
  const res = await fetch('https://api.example.com/data');
  return res.json();
};

useEffect(() => {
  let isMounted = true; // 另一種簡易的 Race Condition 處理方式

  getApiData()
    .then(data => {
      if (isMounted) {
        // 僅在元件仍掛載時更新 State
      }
    })
    .catch(err => console.error(err));

  return () => {
    isMounted = false;
  };
}, []);

3. 常見開發問答 Q&A

Q1:為什麼 Next.js 會顯示 Unhandled Runtime Error 或 n of n unhandled error?

這是因為 Next.js 的開發環境(Overlay)會捕捉所有未處理的執行階段錯誤。當 React 試圖執行 useEffect 回傳的 Promise(誤以為是 destroy 函式)失敗時,就會觸發這個錯誤介面。

Q2:什麼是 Race Condition?為什麼在 useEffect 抓取資料需要處理它?

Race Condition(競態條件)發生在多個非同步操作同時進行,且最後結果取決於它們完成的順序。
例如:使用者快速點擊分頁「1」再點擊分頁「2」。如果分頁「1」的請求因為網路延遲比分頁「2」更晚回傳,UI 可能會先顯示分頁「2」的內容,隨後被分頁「1」的內容覆蓋,導致顯示錯誤。使用 AbortController 或 isMounted 旗標可以確保只有最後一次請求的結果會被套用到 UI 上。

Q3:如果在 useEffect 裡使用 await,一定要寫 try…catch 嗎?

強烈建議加上。因為 useEffect 是一個獨立的執行環境,內部的非同步錯誤若未被捕捉(Unhandled Rejection),可能會導致整個應用程式崩潰或出現難以追蹤的 Bug。

Q4:我可以回傳一個 async 函式作為 Cleanup Function 嗎?

不行。清理函式(Cleanup Function)必須是同步的。如果你在清理階段需要執行非同步操作(例如發送一個 Log 請求),請在同步的清理函式內部啟動該非同步邏輯,而不是讓清理函式本身變成 async。

Q5:使用 then() 會比 async/await 好嗎?

在 useEffect 內部使用 .then().catch() 是完全合法的,且不會產生 destroy is not a function 的問題,因為它不會改變 useEffect 的回傳值。選擇哪種寫法主要取決於團隊的程式碼風格偏好。

參考資料與延伸閱讀

發佈留言