感謝 iT 邦幫忙與博碩文化,本系列文章已出版成書「從 Hooks 開始,讓你的網頁 React 起來」,首刷版稅將全額贊助 iT 邦幫忙鐵人賽,歡迎前往購書,鼓勵筆者撰寫更多優質文章。
到目前為止我們的即時天氣 App 已經可以在載入時自動拉取資料,也可以在使用者點選「重新整理」時重新拉取資料,但是所需的資料還不完整,其中還沒有取得「天氣描述」、「降雨機率」,因此也無法更新天氣圖示。
今天就讓我們來把需要的資料取回來,你一樣可以從昨天在 CodeSandbox 完成的 Weather APP - fetch data with useEffect 繼續,記得需要的話可以複製一份昨天的專案繼續做後面的修改。
由於「降雨機率」這些資料很難用已經取得的那些資料去自己推估,所以勢必還是得要去找看看中央氣象局有沒有提供,如果中央氣象局沒有提供的話,就得要再去找其他第三方的服務了。
好在,回去再翻一下了中央氣象局提供的 API 後發現,在「預報」的地方可以取得降雨機率:
實際透過這道 API 帶入中央氣象局提供的驗證碼後,可以達到下面這樣的資料:
// 透過 'https://opendata.cwb.gov.tw/api/v1/rest/datastore/F-C0032-001' 取得的回應
const data = {
success: 'true',
result: {
// ...
},
records: {
datasetDescription: '三十六小時天氣預報',
location: [
{
locationName: '臺北市',
weatherElement: [
{
elementName: 'Wx',
time: [
{
startTime: '2019-10-04 18:00:00',
endTime: '2019-10-05 06:00:00',
parameter: {
parameterName: '晴時多雲',
parameterValue: '2',
},
},
// ...
],
},
{
elementName: 'PoP',
time: [
{
startTime: '2019-10-04 18:00:00',
endTime: '2019-10-05 06:00:00',
parameter: {
parameterName: '0',
parameterUnit: '百分比',
},
},
// ...
],
},
{
elementName: 'MinT',
time: [
{
startTime: '2019-10-04 18:00:00',
endTime: '2019-10-05 06:00:00',
parameter: {
parameterName: '23',
parameterUnit: 'C',
},
},
// ...
],
},
{
elementName: 'CI',
time: [
{
startTime: '2019-10-04 18:00:00',
endTime: '2019-10-05 06:00:00',
parameter: {
parameterName: '舒適',
},
},
// ...
],
},
{
elementName: 'MaxT',
time: [
{
startTime: '2019-10-04 18:00:00',
endTime: '2019-10-05 06:00:00',
parameter: {
parameterName: '28',
parameterUnit: 'C',
},
},
//..
],
},
],
},
],
},
};
資料內容有點多,讀者可以透過 API 線上說明文件實際上操作看看。
從這些資料中可以取得最近 36 小時的天氣預報,並且將資料切成每 12 小時一份,因此在時間(time
)欄位中,會有三個資料。對照著「預報XML產品預報因子欄位中文說明表」這份文件,可以知道回傳的資料裡面包含「天氣現象(Wx)」、「降雨機率(PoP)」、「舒適度(CI)」、「最高溫度(MaxT)」和「最低溫度(MinT)」:
這真是太好了,我們不只拿到了降雨機率,連天氣現象和舒適度都拿到了。另外回傳的資料和文件中有提供天氣描述代碼,看起來我們還有機會透過這個代碼來顯示對應的天氣圖示:
原本在定義資料狀態 state
的時候,是用 currentWeather
和 setCurrentWeather
:
const [currentWeather, setCurrentWeather] = useState(/* ... */);
但因為現在這個資料中將不只包含當前的天氣資料,還包含從天氣預報中取得的雨量和天氣描述的資料,因此為了避免自己寫到後來混淆,先把資料的命名改一下,改成 weatherElement
:
const [weatherElement, setWeatherElement] = useState(/* ... */);
原本程式中就有使用到 currentWeather
和 setCurrentWeather
的部分,記得也要一併改成 weatherElement
和 setWeatherElement
,如下圖示範:
現在回到專案中一樣可以透過 fetch
來請求天氣預報的資料,寫法會像這樣:
fetch(
'https://opendata.cwb.gov.tw/api/v1/rest/datastore/F-C0032-001?Authorization=[驗證碼]&locationName=臺北市'
)
.then((response) => response.json())
.then((data) => console.log('data', data));
你可以把上面的程式碼帶入自己的驗證碼後,貼到 useEffect()
中,就會看到回傳的結果:
現在如同 fetchCurrentWeather
一樣,來撰寫一個 fetchWeatherForecast
的方法,在這裡面把資料取回來後,並過濾出我們需要的資料。
fetchWeatherForecast
的程式碼會像這樣,邏輯上基本上和 fetchCurrentWeather
是一樣的:
needElements
中定義需要保留的天氣因子item.time[0]
是因為在「未來 36 小時天氣預報」的資料中,會回傳三個時段的資料(每 12 小時一組),因為我們要顯示是即時天氣資訊,所以我們就只取最近的這 12 小時,也就是 time
陣列中的點一個元素setWeatherElement
帶進去 React 組件中(⚠️ 這麽做會有問題,將於後面說明)const fetchWeatherForecast = () => {
fetch(
'https://opendata.cwb.gov.tw/api/v1/rest/datastore/F-C0032-001?Authorization=[驗證碼]&locationName=臺北市'
)
.then((response) => response.json())
.then((data) => {
const locationData = data.records.location[0];
const weatherElements = locationData.weatherElement.reduce(
(neededElements, item) => {
if (['Wx', 'PoP', 'CI'].includes(item.elementName)) {
neededElements[item.elementName] = item.time[0].parameter;
}
return neededElements;
},
{}
);
setWeatherElement({
description: weatherElements.Wx.parameterName,
weatherCode: weatherElements.Wx.parameterValue,
rainPossibility: weatherElements.PoP.parameterName,
comfortability: weatherElements.CI.parameterName,
});
});
};
現在我們把這個寫好的方法,放到 useEffect
中去呼叫,像是這樣:
⚠️ 上面的做法仍不是完整的做法,會有錯誤產生,將於後面進行說明。
但是當我們這樣寫之後,程式很快會發生錯誤:
顯示時間的資料並不是有效格式,感覺實在有點納悶!不然我們先在 return
中把時間的部分註解掉,來看看會怎麼樣:
現在畫面會出現了,但資料的呈現卻還是有點詭異:
為什麼會發生這樣的錯誤呢?
還記得在「Day 16 - 即時天氣 - 定義並請求組件會使用到的資料 - useState 的更多使用」的最後我們有提到,setSomething
這個方法是會把舊有的資料全部清掉,用新的去覆蓋掉,而這也就是這裡問題的原因。
因為現在我們呼叫了兩次不同的 API ,而且在裡面都各自使用的 setWeatherElement
,但我們只把透過 API 取得的資料放進去,而沒有把舊有原本就存在的資料保留下來。時好時壞是因為這兩道 API 回傳資料的速度每次並不一定,而最後取得資料的會把一開始 weatherElement
中的資料覆蓋掉。有時候 fetchCurrentWeather
比較慢得到結果,有時候則是 fetchWeatherForecast
比較慢,所以才會有不一致的情況。
要解決這個問題只需要記得把在 weatherElement
中原有的狀態還去就可以了,在 useState
的 setSomething
這個方法中,除了可以直接帶入新的資料之外,也可以帶入一個函式,這個函式將可以取得前一次的資料狀態,寫法會像這樣:
const [weatherElement, setWeatherElement] = useState(/* ... */)
// 在 setWeatherElement 中也可以帶入函式
// 可以透過這個函式的參數取得前一次的資料狀態
setWeatherElement((prevState => {
// 記得要回傳新的資料狀態回去
return {
...prevState // 保留原有的資料狀態
rainPossibility: 0.1 // 添加或更新的資料
}
}))
套用到實際的專案中會像這樣:
setWeatherElement
中帶入函式,並在函式的參數中帶入 prevState
將可以取得原有的資料狀態return
都不寫,但回傳的物件需要使用小括號 ()
包起來這時候畫面就能夠正確呈現了,最後我們也一併更新一開始使用 useState()
時帶入的預設值,改成像這樣:
const [weatherElement, setWeatherElement] = useState({
observationTime: new Date(),
locationName: '',
humid: 0,
temperature: 0,
windSpeed: 0,
description: '',
weatherCode: 0,
rainPossibility: 0,
comfortability: '',
});
現在畫面已經可以正確呈現,但我們還沒有把透過天氣預報 API 取得的資料都帶入到畫面中,因此在 return
後面會放入新拉取到的資料:
comfortability
)放進去rainPossibility
帶入,同時把 * 100
拿掉,因為現在回傳的資料就已經是百分比onClick
的時候不只要 fetchCurrentWeather
同時還要 fetchWeatherForecast
到目前為止還剩下「天氣圖示」的地方還沒完成第一版就差不多大功告成拉!但現在這個程式碼仍然不是最佳的寫法,我們會在後面幾天繼續優化與說明。
今天完整的程式碼一樣可以在 CodeSandbox 的 Weather APP - fetch multiple data 檢視。
Weather APP - fetch multiple data @ CodeSandbox
Hi, 請問當我使用兩次fetch api時,就算用了prevState,會不會有這樣的狀況:
第一個fetch到了 -> 用之前的prevState去setState -> 還沒setState完成,第二個fetch到了 -> 用一樣的prevState去setState,結果prevState不是最新的
我想我的問題應該是setState是非同步的嗎?
setState 是非同步的,如果是在 setState() 的參數中使用 function,用 prevState 他一定會拿到前一次最新的狀態,但如果你不是在 setState 中用帶入 function 的方式取得 prevState,那麼就有可能會出現你說的情況。