5-7 讓拉取 API 的函式與元件脫鉤
本單元對應的專案分支為:async-function-in-use-effect。
單元核心#
這個單元的主要目標包含:
- 了解可以選擇等待資料回來時一次更新畫面,或分多次更新畫面
- 了解如何在 useEffect 中定義 async function
- 了解如何讓函式與元件解耦,以方便程式碼的拆檔與管理
在上一個單元中,我們已經可以在專案中同時呼叫兩道不同的 API 來取得需要的資料。眼尖的讀者可能會發現,當我們點一次重新整理時,從瀏覽器開發者工具的 console 頁籤中,會發現至少有兩次的畫面更新。
如果你對於導致畫面更新的邏輯夠熟悉的話,應該會想到畫面之所以會更新是因為:
- 呼叫了
useState提供的setWeatherElement方法 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 以更新資料,因此這兩個函式可以進行如下的修改:
- 回傳透過 API 取得的資料,而不用在函式內呼叫
setWeatherElement fetch方法本身即會回傳 Promise,因此這裡可以直接把 fetch 回傳出去(return fetch()),以便後續在 async function 中可以使用
fetchCurrentWeather#
fetchWeatherForecast#
在 useEffect 中建立 async function 來等待資料回應#
現在原本的 fetchCurrentWeather 和 fetchWeatherForecast 都不會在其內部呼叫 setWeatherElement,而是把透過 API 取得的資料回傳出來。因此可以將原本 useEffect 中的函式進行修改:
- 在 useEffect 的函式中定義 async function,取名為
fetchData,在這個 function 中會同時呼叫fetchCurrentWeather和fetchWeatherForecast - 由於
fetchCurrentWeather和fetchWeatherForecast這兩個函式呼叫後,會回傳 Promise,因此透過 async function 中的await語法搭配Promise.all就可以等待該函式中 fetch API 的資料都取得回應後才讓程式碼繼續往後走 - 透過
console.log檢視取得的資料 - 最後,在
useEffect中執行定義好的fetchData這個函式
進行元件資料狀態更新#
由於 Promise.all 回傳的資料會是陣列,而陣列中的元素依序就會是 Promise.all([]) 中各個 Promise 回傳的內容,因此可以直接透過陣列的解構賦值來取出 await Promise.all() 所回傳的資料,並放入 setWeatherElement 中來更新元件的資料狀態:
處理資料載入中的狀態#
另外,開始透過 AJAX 拉取資料前,需要先把 isLoading 的狀態改成 true,因此在 fetchData 的一開始,會先透過 setWeatherElement 將 isLoading 的狀態設為 true:
重點:當 function 不依賴 state 時,可以將 function 定義在 App 元件外#
在這個單元中,我們透過 async function 的方式,等到兩支 API 的資料都得到回應後,才去呼叫 setWeatherElement 更新畫面,如此,使用者便不會感受到資料分成了兩次進來。
除了使用者的體驗外,還有一個重點,在前一個單元中,因為 fetchCurrentWeather 和 fetchWeatherForecast 中都會呼叫到 setWeatherElements 這個方法,也就是說,這兩個方法會需要使用到 App 元件中 useState 回傳的 setWeatherElement,因此當時並沒有辦法把這兩個方法拉到 App 元件外去定義。
但現在因為 fetchCurrentWeather 和 fetchWeatherForecast 都已經不再依賴 useState 提供的 weatherElement 或 setWetherElements 的方法,因此可以自由地搬到 App 元件外,它就像一個獨立的 JavaScript 函式一樣,為了管理上的方便,你也可以把它放到不同的 JavaScript 的檔案中,再透過 import 載入進來使用即可。

這一點對於專案程式碼的管理上很有幫助,把拉取資料的方法和元件本身拆分開來,可以避免元件的程式碼過於龐雜,並且可以將不同支拉取 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
