useEffect 搭配 async/await 正確寫法與實作範例
為什麼 useEffect 不能直接用 async?
當你這樣寫:
useEffect(async () => {
const data = await fetchData();
// 處理資料
}, []);
會出現錯誤,因為 async 函數回傳一個 Promise,而 useEffect 期望的是一個普通函數或清理函數(cleanup function)。這會導致 React 無法正確處理副作用。
修正方法:將 async 函數定義在 useEffect 內部
正確的做法是在 useEffect 的回調函數內部定義一個 async 函數,然後立即執行它。這樣可以保持 useEffect 的回調函數符合規範,同時使用 async/await 來處理非同步操作。
步驟:如何正確撰寫
-
定義一個內部的 async 函數:在 useEffect 的回調函數中,定義一個獨立的 async 函數來處理非同步邏輯。
-
立即執行該函數:在定義後立即呼叫這個 async 函數。
-
處理清理函數(如果需要):如果有需要清理的副作用(例如取消訂閱或清除計時器),確保回傳一個普通函數作為清理函數。
-
管理依賴陣列:確保 useEffect 的依賴陣列正確設置,避免不必要的重複執行。
完整範例程式碼
假設你要從一個 API 抓取資料並更新元件狀態,這是正確的寫法:
import React, { useState, useEffect } from 'react';
function MyComponent() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// 定義內部的 async 函數
const fetchData = async () => {
try {
setLoading(true); // 開始載入
const response = await fetch('https://api.example.com/data'); // 假設的 API 網址
if (!response.ok) {
throw new Error('網路請求失敗');
}
const result = await response.json();
setData(result); // 更新狀態
} catch (err) {
setError(err.message); // 處理錯誤
} finally {
setLoading(false); // 結束載入
}
};
// 立即執行 async 函數
fetchData();
// 可選:回傳清理函數
return () => {
console.log('清理副作用,例如取消訂閱或清除計時器');
// 在這裡執行清理邏輯(如果需要)
};
}, []); // 空陣列表示僅在元件掛載時執行一次
if (loading) return <div>載入中...</div>;
if (error) return <div>錯誤:{error}</div>;
return (
<div>
<h1>從 API 取得的資料</h1>
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
</div>
);
}
export default MyComponent;
程式碼說明
-
導入必要的 Hook:
-
使用 useState 管理資料、載入狀態和錯誤訊息。
-
使用 useEffect 處理副作用(例如 API 請求)。
-
-
定義 async 函數:
-
在 useEffect 內定義 fetchData 函數,使用 async/await 處理非同步的 fetch 請求。
-
使用 try/catch 處理可能的錯誤,確保程式碼健壯。
-
在 finally 區塊中更新載入狀態,確保無論成功或失敗,載入狀態都會被重置。
-
-
立即執行 fetchData:
- 在 useEffect 內直接呼叫 fetchData(),啟動非同步操作。
-
清理函數:
-
useEffect 回傳一個清理函數(在這個例子中只是簡單的 console.log),用來處理元件卸載時的清理工作(例如取消 API 請求或清除計時器)。
-
如果你的 API 請求需要取消(例如使用 AbortController),可以在這裡實現。
-
-
依賴陣列:
-
依賴陣列設為 [],表示 useEffect 只在元件掛載時執行一次。
-
如果你的非同步操作依賴某些變數(例如 userId),記得將其加入依賴陣列,例如 [userId]。
-
-
渲染邏輯:
-
根據 loading 和 error 狀態顯示不同的 UI。
-
當資料成功取得後,使用 JSON.stringify 顯示資料(這是為了展示,你可以根據需求自訂渲染方式)。
-
常見問題與解決方法
-
問題:為什麼我的 useEffect 重複執行?
-
原因:依賴陣列中的變數發生變化,導致 useEffect 重新執行。
-
解決方法:檢查依賴陣列,確保只包含必要的依賴項。如果不需要重複執行,設為空陣列 []。
-
-
問題:如何取消 API 請求?
-
解決方法:使用 AbortController 來取消非同步請求。以下是範例:
import React, { useState, useEffect } from 'react';
function MyComponent() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController(); // 建立 AbortController
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch('https://api.example.com/data', {
signal: controller.signal, // 綁定信號
});
if (!response.ok) {
throw new Error('網路請求失敗');
}
const result = await response.json();
setData(result);
} catch (err) {
if (err.name === 'AbortError') {
console.log('請求已被取消');
} else {
setError(err.message);
}
} finally {
setLoading(false);
}
};
fetchData();
// 清理函數:取消請求
return () => {
controller.abort();
};
}, []);
if (loading) return <div>載入中...</div>;
if (error) return <div>錯誤:{error}</div>;
return (
<div>
<h1>從 API 取得的資料</h1>
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
</div>
);
}
export default MyComponent; -
說明:這裡使用 AbortController 來管理請求,當元件卸載時,清理函數會呼叫 controller.abort() 取消請求,避免記憶體洩漏或不必要的狀態更新。
-
-
問題:如何在依賴變更時重新抓取資料?
-
解決方法:將變數加入依賴陣列。例如,如果資料依賴 userId:
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(`https://api.example.com/data/${userId}`);
if (!response.ok) {
throw new Error('網路請求失敗');
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
return () => {
console.log('清理副作用');
};
}, [userId]); // 當 userId 改變時重新執行
-
注意事項
-
錯誤處理:始終使用 try/catch 來處理非同步操作的錯誤,避免程式崩潰。
-
清理副作用:如果你的副作用涉及訂閱、計時器或非同步請求,記得在清理函數中妥善處理。
-
依賴陣列:確保依賴陣列正確設置,避免無限迴圈或不必要的重新渲染。
-
避免直接將 async 用於 useEffect:這是常見錯誤,記得總是在內部定義 async 函數。