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 後更多人使用的則是 Promiseasync function,這兩種語法都可以讓程式碼的語意更清楚,在讀起來時更容易理解,同時還可以搭配使用。

提示

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

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

原本我們是在 fetchCurrentWeatherfetchWeatherForecast 這兩個函式中,各自呼叫 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 來等待資料回應#

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

  1. 在 useEffect 的函式中定義 async function,取名為 fetchData,在這個 function 中會同時呼叫 fetchCurrentWeatherfetchWeatherForecast
  2. 由於 fetchCurrentWeatherfetchWeatherForecast 這兩個函式呼叫後,會回傳 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 的一開始,會先透過 setWeatherElementisLoading 的狀態設為 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 更新畫面,如此,使用者便不會感受到資料分成了兩次進來。

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

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

Imgur

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

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

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

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

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

  • fetchCurrentWeatherfetchWeatherForecast 直接回傳 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