感謝 iT 邦幫忙與博碩文化,本系列文章已出版成書「從 Hooks 開始,讓你的網頁 React 起來」,首刷版稅將全額贊助 iT 邦幫忙鐵人賽,歡迎前往購書,鼓勵筆者撰寫更多優質文章。
昨天的我們已經可以專案中同時呼叫兩道不同的 API 來取得我們需要的資料,眼尖的朋友可能會發現,當我們試圖更新一次資料時,實際上 React 組件實際上被呼叫了兩次,因此也兩次畫面的渲染:
如果你對於導致畫面更新的邏輯夠熟悉的話,應該會想到畫面之所以會更新是因為:
useState
提供的 setWeatherElement
方法setWeatherElement
設進去的資料的確有改變而在昨天的程式碼中,因為要拉取不同來源的 API 資料,所以呼叫了兩次 fetch
API,並在 fetch
取得資料後,各自一併呼叫 setWeatherElement
了 API,而這也就是畫面之所以會渲染兩次的原因。
如果對於畫面更新的邏輯還不太熟悉的話,可以回頭複習 Day 06 - 醒醒啊!為什麼一動也不動 - useState 的基本使用。
其實上面這種做法並沒有錯,但在畫面的呈現上,如果因為使用者網路狀況不好,或其他原因導致兩道 API 回傳資料的速度不一樣的話,畫面就會變得詭異,因為對使用者來說明明是按一次資料更新,但卻會發現畫面上的資料分了兩次進來,畫面看起來大概會變成這樣:
因此在這裡比較好的做法其實應該是等到拿完全部的資料後,使用一次 setWeatherElement
把所有拿到的資料給進去,這時候使用者就只會看到一次畫面的更新。
但並不是每種狀況都要等全部的資料回來才顯示給使用者看,因為這樣做就反而喪失了使用 AJAX 拉取資料的好處,舉例來說,當我們在瀏覽電商網站時,好的使用者體驗它不會等到所有資料都載進來之後才顯示網頁,而是會先呈現一個外框的畫面但內容很多是灰灰底尚未載入,等到 API 資料回來後才把圖片顯示出來,甚至是等到使用者的捲軸滾到該頁面時才去拉取資料並顯示:
因此實際上應該要等到所有資料都取得後才一次呈現出來,還是資料回來就馬上呈現,端看畫面的內容量和設計呈現。在我們即時天氣 App 中,因為資料量不大,所以等到兩個資料都回來後才呈現,並不會讓使用者等待太久,同時也不會導致使用者覺得點一次按鈕卻不同步的更新了兩次畫面。
現在,我可以把昨天的程式碼改成等到兩個 API 資料都回來後才呼叫 setWeatherElement
去重新渲染畫面。在 JavaScript 中,要做這種「等待」或者說是「當...後,才能...」這種動作時,過去最常使用的是回呼函式(callback function),在 ES6 後更多人使用的則是 Promise 和 async function
,這兩種語法都可以讓程式碼的語意更清楚,在讀起來時更容易理解,同時還可以搭配使用。
如果你對於 Promise 或 async function 的用法還不太清楚,可以從下方參考資源中點選 MDN 的說明文件,或者直接透過 Google 可以找到非常非常非常多說明資料。
這裡我們就來用 async function 來達成,你可以在 CodeSandbox 中打開昨天的專案 Weather APP - fetch multiple data
因為程式碼改變的量稍大,分兩部分來完成。
第一個部分主要是拉取資料,做法會像這樣:
fetchData
,在這個 function 中會同時呼叫兩道 fetch APIawait
語法搭配 Promise.all
就可以等待 fetch API 的資料都回應後才讓程式碼繼續往後走fetch
API 原本就會回傳 Promise,因此在 fetchCurrentWeather
和 fetchWeatherForecast
中,不是只是呼叫 fetch 方法,而是把 fetch
方法呼叫後得到的 Promise 回傳出去fetchCurrentWeather
和 fetchWeatherForecast
的函式中,不直接去 setWeatherElement
而是把取得的資料回傳出去useEffect
中執行定義好的 fetchData
這個函式// ./src/WeatherApp.js
// ...
const WeatherApp = () => {
const [weatherElement, setWeatherElement] = useState(/* ... */);
useEffect(() => {
// STEP 1:在 useEffect 中定義 async function 取名為 fetchData
const fetchData = async () => {
// STEP 2:使用 Promise.all 搭配 await 等待兩個 API 都取得回應後才繼續
const data = await Promise.all([
fetchCurrentWeather(),
fetchWeatherForecast(),
]);
console.log('data', data);
};
// STEP 5:呼叫 fetchData 這個方法
fetchData();
}, []);
const fetchCurrentWeather = () => {
// STEP 3-1:加上 return 直接把 fetch API 回傳的 Promise 回傳出去
return fetch(/* ... */)
.then((response) => response.json())
.then((data) => {
// ...
// STEP 3-2:把取得的資料內容回傳出去,而不是在這裡 setWeatherElement
return {
observationTime: locationData.time.obsTime,
locationName: locationData.locationName,
temperature: weatherElements.TEMP,
windSpeed: weatherElements.WDSD,
humid: weatherElements.HUMD,
};
});
};
const fetchWeatherForecast = () => {
// STEP 4-1:加上 return 直接把 fetch API 回傳的 Promise 回傳出去
return fetch(/* ... */)
.then((response) => response.json())
.then((data) => {
// ...
// STEP 4-2:把取得的資料內容回傳出去,而不是在這裡 setWeatherElement
return {
description: weatherElements.Wx.parameterName,
weatherCode: weatherElements.Wx.parameterValue,
rainPossibility: weatherElements.PoP.parameterName,
comfortability: weatherElements.CI.parameterName,
};
});
};
return {
/* ... */
};
};
export default WeatherApp;
完成後,順利的話在瀏覽器的開發者工具中將會看到取得的資料現在包在一個陣列中,分別對應到 fetchCurrentWeather
和 fetchWeatherForecast
從 API 取得的資料:
接著可以透過解構賦值來將資料從陣列中取出,並且放入 setWeatherElement
的物件中:
currentWeather
和 weatherForecast
setWeatherElement
的物件中// ./src/WeatherApp.js
const WeatherApp = () => {
// ...
useEffect(() => {
const fetchData = async () => {
// STEP 6:使用陣列的解構賦值把資料取出
const [currentWeather, weatherForecast] = await Promise.all([
fetchCurrentWeather(),
fetchWeatherForecast(),
]);
// STEP 7:把取得的資料透過物件的解構賦值放入
setWeatherElement({
...currentWeather,
...weatherForecast,
});
};
fetchData();
}, []);
// ...
};
將程式碼改成這樣後,就只會在取得所有資料後觸發一次畫面的更新,一樣可以在 CodeSandbox 檢視完整的程式碼 - Weather APP - fetch data with async function in useEffect。
雖然上面我們這樣寫下來好像很順,但也許可以想想看為什麼我們是把 fetchData
這個函式定義在 useEffect
中而不是先定義在 useEffect
外定義好,在裡面呼叫就好呢?如果有興趣的話,你可以把 fetchData
的函式搬到 useEffect
外面試試看程式是否可以正常運作?看看瀏覽器的開發者工具中有沒有出現任何錯誤提示?
明天我們會再接著繼續討論這個部分!
Weather APP - fetch data with async function in useEffect