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