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

在 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)與程式碼範本快速跳轉
- 為什麼會出現 destroy is not a function?
- 解法一:在 useEffect 內部宣告 async 函式與處理 Race Condition(推薦)
- 解法二:使用 IIFE 立即執行函式
- 解法三:將非同步邏輯抽離至外部函式
- 常見開發問答 Q&A:如何處理 Race Condition?
核心機制對照表:useEffect 預期值 vs. Async 函式
| 維度 | useEffect 回傳要求 | async 函式回傳值 | 衝突點 |
|---|---|---|---|
| 資料型別 | 必須是 function (清理函式) 或 undefined | 永遠回傳一個 Promise 物件 | React 預期收到函式,卻收到 Promise |
| 執行時機 | 元件卸載 (Unmount) 或依賴項改變前 | 呼叫後立即回傳 Promise | React 試圖執行 Promise 物件導致報錯 |
| 報錯訊息 | 無 | TypeError: destroy is not a function | Promise 物件沒有執行能力 |
1. 錯誤原因解析:為什麼會報錯?
在 React 的設計中,useEffect 的第一個參數(Effect Callback)只允許回傳兩種東西:
- undefined(不回傳任何內容)。
- 一個函式(稱為 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 的回傳值。選擇哪種寫法主要取決於團隊的程式碼風格偏好。
參考資料與延伸閱讀
- React Reference: useEffect Hook – 參數與清理函式規範
- React Learn: Synchronizing with Effects – 處理副作用清理邏輯
- MDN Web Docs: async function – 非同步函式回傳值規範
- MDN Web Docs: AbortController.abort() – 取消非同步請求實作
- React Learn: Fetching data in Effects – 處理競態條件與資料抓取範式
- 延伸閱讀:React 框架中 useEffect 勾子函式之 Unhandled Runtime Error: TypeError: destroy is not a function 異常診斷與修復規範
- 延伸閱讀:解決 React 元件的 defaultProps 錯誤:轉移到 JavaScript 預設參數
發佈留言