Skip to main content

useEffect 搭配 async/await 正確寫法與實作範例

為什麼 useEffect 不能直接用 async?

當你這樣寫:

useEffect(async () => {
const data = await fetchData();
// 處理資料
}, []);

會出現錯誤,因為 async 函數回傳一個 Promise,而 useEffect 期望的是一個普通函數或清理函數(cleanup function)。這會導致 React 無法正確處理副作用。

修正方法:將 async 函數定義在 useEffect 內部

正確的做法是在 useEffect 的回調函數內部定義一個 async 函數,然後立即執行它。這樣可以保持 useEffect 的回調函數符合規範,同時使用 async/await 來處理非同步操作。

步驟:如何正確撰寫

  1. 定義一個內部的 async 函數:在 useEffect 的回調函數中,定義一個獨立的 async 函數來處理非同步邏輯。

  2. 立即執行該函數:在定義後立即呼叫這個 async 函數。

  3. 處理清理函數(如果需要):如果有需要清理的副作用(例如取消訂閱或清除計時器),確保回傳一個普通函數作為清理函數。

  4. 管理依賴陣列:確保 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;

程式碼說明

  1. 導入必要的 Hook

    • 使用 useState 管理資料、載入狀態和錯誤訊息。

    • 使用 useEffect 處理副作用(例如 API 請求)。

  2. 定義 async 函數

    • 在 useEffect 內定義 fetchData 函數,使用 async/await 處理非同步的 fetch 請求。

    • 使用 try/catch 處理可能的錯誤,確保程式碼健壯。

    • 在 finally 區塊中更新載入狀態,確保無論成功或失敗,載入狀態都會被重置。

  3. 立即執行 fetchData

    • 在 useEffect 內直接呼叫 fetchData(),啟動非同步操作。
  4. 清理函數

    • useEffect 回傳一個清理函數(在這個例子中只是簡單的 console.log),用來處理元件卸載時的清理工作(例如取消 API 請求或清除計時器)。

    • 如果你的 API 請求需要取消(例如使用 AbortController),可以在這裡實現。

  5. 依賴陣列

    • 依賴陣列設為 [],表示 useEffect 只在元件掛載時執行一次。

    • 如果你的非同步操作依賴某些變數(例如 userId),記得將其加入依賴陣列,例如 [userId]。

  6. 渲染邏輯

    • 根據 loading 和 error 狀態顯示不同的 UI。

    • 當資料成功取得後,使用 JSON.stringify 顯示資料(這是為了展示,你可以根據需求自訂渲染方式)。

常見問題與解決方法

  1. 問題:為什麼我的 useEffect 重複執行?

    • 原因:依賴陣列中的變數發生變化,導致 useEffect 重新執行。

    • 解決方法:檢查依賴陣列,確保只包含必要的依賴項。如果不需要重複執行,設為空陣列 []。

  2. 問題:如何取消 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() 取消請求,避免記憶體洩漏或不必要的狀態更新。

  3. 問題:如何在依賴變更時重新抓取資料?

    • 解決方法:將變數加入依賴陣列。例如,如果資料依賴 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 函數。