5-7 讓拉取 API 的函式與元件脫鉤

本單元對應的專案分支為:async-function-in-use-effect。

單元核心#

這個單元的主要目標包含:

  • 了解可以選擇等待資料回來時一次更新畫面,或分多次更新畫面
  • 了解如何在 useEffect 中定義 async function
  • 了解如何讓函式與元件解耦,以方便程式碼的拆檔與管理

在上一個單元中,我們已經可以在專案中同時呼叫兩道不同的 API 來取得需要的資料。眼尖的讀者可能會發現,當我們點一次重新整理時,從瀏覽器開發者工具的 console 頁籤中,會發現至少有兩次的畫面更新。

如果你對於導致畫面更新的邏輯夠熟悉的話,應該會想到畫面之所以會更新是因為:

  1. 呼叫了 useState 提供的 setWeatherElement 方法
  2. setWeatherElement 寫進去的資料的確有改變

而在上一個章節的程式碼中,因為要拉取不同來源的 API 資料,所以呼叫了兩次 fetch API,並在 fetch 取得資料後,各自一併呼叫 setWeatherElement 了 API,而這也就是畫面之所以會轉譯兩次的原因。

根據使用時機選擇一次呈現或分別呈現#

其實上面這種做法並沒有錯,但在畫面的呈現上,如果因為使用者網路狀況不好,或其他原因導致兩支 API 回傳資料的速度不一樣的話,畫面就會變得詭異,因為對使用者來說明明是按一次資料更新,但卻會發現畫面上的資料分了兩次進來。

在這裡比較好的做法應該是等到拿完全部的資料後,使用一次 setWeatherElement 把所有拿到的資料給進去,這時候使用者就只會看到一次畫面的更新。

但並不是每種狀況都要等全部的資料回來才顯示給使用者看,因為這樣做就有時會喪失了使用 AJAX 分別拉取資料的好處,舉例來說,當我們在瀏覽電商網站時,好的使用者體驗不會等到所有資料都載進來之後才顯示網頁,而是會先呈現一個外框的畫面但內容很多是灰底且尚未載入的,等到 API 資料回傳後才把圖片依序顯示出來,甚至是等到使用者的捲軸滾到該頁面時才去拉取資料並顯示。

因此實要等到所有資料都取得後才一次呈現,或是資料回來就馬上呈現,端看畫面的內容量和設計而定。在我們的「臺灣好天氣」中,因為資料量不大,所以等到兩個資料都回來後才呈現,並不會讓使用者等待太久,同時也不會導致使用者覺得點一次按鈕卻不同步的更新了兩次畫面。

透過 async 和 Promise 拉取並等待資料回應#

我們可以把程式改成等到兩支 API 資料都回來後才呼叫 setWeatherElement 去重新轉譯畫面。在 JavaScript 中,要做這種「等待」或者說是「當......後,才能......」這種動作時,過去最常使用的是回呼函式(callback function),在 ES6 後更多人使用的則是 Promise 和 async function,這兩種語法都可以讓程式碼的語意更清楚,在讀起來時更容易理解,同時還可以搭配使用。

提示

如果你對於 Promise 或 async function 的用法還不太清楚,可以到本單元 Github 專案說明頁中的連結,或者直接透過 Google 可以找到非常多的說明資料。

修改 fetchCurrentWeather 和 fetchWeatherForecast 讓其回傳帶有資料的 Promise#

原本我們是在 fetchCurrentWeather 和 fetchWeatherForecast 這兩個函式中,各自呼叫 setWeatherElement 來更新元件內的資料狀態,現在因為我們希望等到這兩支 API 都取得回應後才來呼叫 setWeatherElement 以更新資料,因此這兩個函式可以進行如下的修改:

  1. 回傳透過 API 取得的資料,而不用在函式內呼叫 setWeatherElement
  2. fetch 方法本身即會回傳 Promise,因此這裡可以直接把 fetch 回傳出去(return fetch()),以便後續在 async function 中可以使用

fetchCurrentWeather#

// ./src/App.js
const fetchCurrentWeather = () => {
// 留意這裡加上 return 直接把 fetch API 回傳的 Promise 再回傳出去
return fetch(/* ... */)
.then((response) => response.json())
.then((data) => {
// ...
// 把取得的資料內容回傳出去,而不是在這裡 setWeatherElement
return {
observationTime: locationData.time.obsTime,
locationName: locationData.locationName,
temperature: weatherElements.TEMP,
windSpeed: weatherElements.WDSD,
};
});
};
const App = () => {
/* ... */
};

fetchWeatherForecast#

// ./src/App.js
const fetchCurrentWeather = () => {
/* ... */
};
// 留意這裡加上 return 直接把 fetch API 回傳的 Promise 再回傳出去
const fetchWeatherForecast = () => {
return fetch(/* ... */)
.then((response) => response.json())
.then((data) => {
// ...
// 把取得的資料內容回傳出去,而不是在這裡 setWeatherElement
return {
description: weatherElements.Wx.parameterName,
weatherCode: weatherElements.Wx.parameterValue,
rainPossibility: weatherElements.PoP.parameterName,
comfortability: weatherElements.CI.parameterName,
};
});
};
const App = () => {
/* ... */
};

在 useEffect 中建立 async function 來等待資料回應#

現在原本的 fetchCurrentWeather 和 fetchWeatherForecast 都不會在其內部呼叫 setWeatherElement,而是把透過 API 取得的資料回傳出來。因此可以將原本 useEffect 中的函式進行修改:

  1. 在 useEffect 的函式中定義 async function,取名為 fetchData,在這個 function 中會同時呼叫 fetchCurrentWeather 和 fetchWeatherForecast
  2. 由於 fetchCurrentWeather 和 fetchWeatherForecast 這兩個函式呼叫後,會回傳 Promise,因此透過 async function 中的 await 語法搭配 Promise.all就可以等待該函式中 fetch API 的資料都取得回應後才讓程式碼繼續往後走
  3. 透過 console.log 檢視取得的資料
  4. 最後,在 useEffect 中執行定義好的 fetchData 這個函式
// ./src/App.js
const fetchCurrentWeather = () => {
/* ... */
};
const fetchWeatherForecast = () => {
/* ... */
};
const App = () => {
// ...
useEffect(() => {
// STEP 1:在 useEffect 中定義 async function 取名為 fetchData
const fetchData = async () => {
// STEP 2:使用 Promise.all 搭配 await 等待兩個 API 都取得回應後才繼續
const data = await Promise.all([
fetchCurrentWeather(),
fetchWeatherForecast(),
]);
// STEP 3:檢視取得的資料
console.log(data);
};
// STEP 4:再 useEffect 中呼叫 fetchData 方法
fetchData();
}, []);
// ...
};

進行元件資料狀態更新#

由於 Promise.all 回傳的資料會是陣列,而陣列中的元素依序就會是 Promise.all([]) 中各個 Promise 回傳的內容,因此可以直接透過陣列的解構賦值來取出 await Promise.all() 所回傳的資料,並放入 setWeatherElement 中來更新元件的資料狀態:

// ./src/App.js
useEffect(() => {
const fetchData = async () => {
// 直接透過陣列的解構賦值來取出 Promise.all 回傳的資料
const [currentWeather, weatherForecast] = await Promise.all([
fetchCurrentWeather(),
fetchWeatherForecast(),
]);
// 把取得的資料透過物件的解構賦值放入
setWeatherElement({
...currentWeather,
...weatherForecast,
isLoading: false,
});
};
fetchData();
}, []);

處理資料載入中的狀態#

另外,開始透過 AJAX 拉取資料前,需要先把 isLoading 的狀態改成 true,因此在 fetchData 的一開始,會先透過 setWeatherElement 將 isLoading 的狀態設為 true:

// ./src/App.js
useEffect(() => {
const fetchData = async () => {
// 在開始拉取資料前,先把 isLoading 的狀態改成 true
setWeatherElement((prevState) => ({
...prevState,
isLoading: true,
}));
const [currentWeather, weatherForecast] = await Promise.all([
fetchCurrentWeather(),
fetchWeatherForecast(),
]);
// ...
};
fetchData();
}, []);

重點:當 function 不依賴 state 時,可以將 function 定義在 App 元件外#

在這個單元中,我們透過 async function 的方式,等到兩支 API 的資料都得到回應後,才去呼叫 setWeatherElement 更新畫面,如此,使用者便不會感受到資料分成了兩次進來。

除了使用者的體驗外,還有一個重點,在前一個單元中,因為 fetchCurrentWeather 和 fetchWeatherForecast 中都會呼叫到 setWeatherElements 這個方法,也就是說,這兩個方法會需要使用到 App 元件中 useState 回傳的 setWeatherElement,因此當時並沒有辦法把這兩個方法拉到 App 元件外去定義。

但現在因為 fetchCurrentWeather 和 fetchWeatherForecast 都已經不再依賴 useState 提供的 weatherElement 或 setWetherElements 的方法,因此可以自由地搬到 App 元件外,它就像一個獨立的 JavaScript 函式一樣,為了管理上的方便,你也可以把它放到不同的 JavaScript 的檔案中,再透過 import 載入進來使用即可。

Imgur

這一點對於專案程式碼的管理上很有幫助,把拉取資料的方法和元件本身拆分開來,可以避免元件的程式碼過於龐雜,並且可以將不同支拉取 API 的方法進行拆檔管理。

在這個單元中,我們先只處理使用者初次載入頁面時的情況,下一個單元會再來處理當使用者點擊重新整理按鈕時的情況。

換你了!將拉取 API 的兩個函式獨立於元件,並搭配 async...await 取得資料。#

這個單元使用了較多 JavaScript 在處理非同步請求前的進階語法,像是 Promise 或 async...await,而這也是 JavaScript 在處理 AJAX 資料請求時非常重要的知識,若讀者對於這個部分較不熟悉的話,這個單元讀起來可能會相當吃力,讀者可以選擇先跟著提供的程式碼實作,把後續的部分完成後,未來再把這個部分補齊。

現在,請你將拉取 API 的兩個函式獨立於 App 元件之外,接著透過 async 函式搭配 Promise 來做到當兩支 API 的資料都回來時,才將資料呈現給使用者。你可以參考下述流程,試著實際操作看看:

  • 讓 fetchCurrentWeather 和 fetchWeatherForecast 直接回傳 fetch,並可以取得 API 回應的資料,而不是直接在函式內呼叫 setWeatherElement
  • 在 useEffect 中建立名為 fetchData 的 async function
  • 在 fetchData 中透過 await Promise.all() 的語法,取得兩支 API 回應的結果
  • 在 useEffect 內呼叫 fetchData 方法
  • 將 fetchData 取得的資料透過 setWeatherElement 來更新元件內的資料狀態
  • 處理資料載入中的 isLoading 狀態,在 fetchData 的最開始,把 isLoading 設為 true

本單元相關之網頁連結、完整程式碼與程式碼變更部分可於 async-function-in-use-effect 分支檢視:https://github.com/pjchender/learn-react-from-hook-realtime-weather-app/tree/async-function-in-use-effect

Imgur