5-6 搭配 useEffect 拉取多支 API 回傳的資料

本單元對應的專案分支為:fetch-forecast-data

單元核心#

這個單元的主要目標包含:

  • 了解如何搭配 useEffect 拉取多支 API 回傳的資料
  • 在 setSomething 中代入函式,以取得原本的資料狀態

到目前為止「臺灣好天氣」已經可以在載入時自動拉取資料,也可以在使用者點選「重新整理」時重新拉取資料,但是所需的資料還不完整,其中還沒有取得「天氣描述」、「降雨機率」,因此也無法更新天氣圖示。

在這個單元中,我們會使用中央氣象局提供另一支「天氣預報 API」來取得不足的資訊,並且學習在 React 元件中,如何一次發送多支 API 請求。

了解 API 回傳的天氣預報資料#

為了要取得「降雨機率」與「天氣描述」的資料,這裡會使用到前面曾說明過「一般天氣預報-今明 36 小時天氣預報」這支 API。同樣可以在線上說明文件試打「/v1/rest/datastore/F-C0032-001 一般天氣預報-今明 36 小時天氣預報」這支 API 來取得回應:

imgur

點擊「Try it out」,填入授權碼後,看看這支 API 會回應的資料內容。

從回應的內容中可以看到,我們一樣可以從 records.location 中取得和天氣有關的資料:

{
"success": "true",
"result": {
/* ... */
},
"records": {
"datasetDescription": "三十六小時天氣預報",
"location": [/* ... */]
}
}

一樣在 location 屬性中的 weatherElement 中,可以看到提供了很多不同類型的資料:

{
"weatherElement": [
{
"elementName": "PoP",
"time": [
{
"startTime": "2020-06-28 18:00:00",
"endTime": "2020-06-29 06:00:00",
"parameter": {
"parameterName": "0",
"parameterUnit": "百分比"
}
}, // ...
]
}, // ...
]
}

從這些資料中可以取得最近 36 小時的天氣預報,並且將資料切成每 12 小時一份,因此在時間(time)欄位中,一共會有三個資料。

對照著「預報 XML 產品預報因子欄位中文說明表(https://opendata.cwb.gov.tw/opendatadoc/MFC/D0047.pdf)」這份文件,可以知道回傳的資料裡面包含「天氣現象(Wx)」、「降雨機率(PoP)」、「舒適度(CI)」、「最高溫度(MaxT)」和「最低溫度(MinT)」:

imgur

也就是說,透過天氣預報這支 API 我們不只拿到了「降雨機率」,同時也可以透過「天氣現象」和「舒適度」來組成畫面中所需的「天氣描述」。另外在「天氣現象」回傳的資料中,還提供了天氣描述代碼(weatherCode),後續將可以透過這個代碼來顯示對應的「天氣圖示」:

imgur

透過 fetch 取得天氣預報資料#

現在我們就可以透過剛剛找到的這支 API 來填補當初資料不足的部分。

修改資料狀態的名稱#

原本在定義資料狀態 state 的時候,是用 currentWeathersetCurrentWeather

const [currentWeather, setCurrentWeather] = useState(/* ... */);

但現在這個資料中不只包含當前的天氣資料,還包含從天氣預報中取得的雨量和天氣描述的資料,為了避免自己寫到後來混淆,先把資料的命名改成 weatherElement

const [weatherElement, setWeatherElement] = useState(/* ... */);

原本程式中就有使用到 currentWeathersetCurrentWeather 的部分,記得也要一併改成 weatherElementsetWeatherElement,如下圖所示:

Imgur

撰寫 fetch 程式碼#

現在回到專案中一樣可以透過 fetch 請求天氣預報的資料,寫法會像這樣:

const LOCATION_NAME_FORECAST = '臺北市';
fetch(
`https://opendata.cwb.gov.tw/api/v1/rest/datastore/F-C0032-001?Authorization=${AUTHORIZATION_KEY}&locationName=${LOCATION_NAME_FORECAST}`
)
.then((response) => response.json())
.then((data) => console.log('data', data));
留意

這裡我們額外定義了一個變數名稱為 LOCATION_NAME_FORECAST,值是「臺北市」,還記得前面曾經提過「天氣觀測」和「天氣預報」需要填入的 locationName 不同,「天氣觀測」要帶入的是「局屬觀測站」,而「天氣預報」要帶入是「縣市名稱」。這裡因為是呼叫「天氣預報」的 API,因此需要帶入的是「臺北市」而不是「臺北」,否則會無法正確取得資料。

撰寫呼叫天氣預報 API 的函式#

如同 fetchCurrentWeather 一樣,現在來撰寫一個 fetchWeatherForecast 的方法,把資料取回來後,過濾出我們需要的資料。

fetchWeatherForecast 的程式碼,邏輯基本上和 fetchCurrentWeather 是一樣的:

  1. 透過 reduce 過濾出所需要的天氣因子,包含「天氣現象(Wx)」、「降雨機率(PoP)」和「舒適度(CI)」。
  2. 這裡之所以使用了 item.time[0] 是因為在「未來 36 小時天氣預報」的資料中,會回傳三個時段的資料(每 12 小時一組),而我們要顯示的是即時天氣資訊,所以我們就只取最接近的 12 小時預報資料,也就是 time 陣列中的第一個元素:
const fetchWeatherForecast = () => {
fetch(/*...*/)
.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)) {
// 這支 API 會回傳未來 36 小時的資料,這裡只需要取出最近 12 小時的資料,因此使用 item.time[0]
neededElements[item.elementName] = item.time[0].parameter;
}
return neededElements;
},
{}
);
});
};
  1. 把資料透過 setWeatherElement 更新 React 元件的資料狀態中,但這麽做會有一些問題,將於後面說明
// ⚠️ 這麼做會有問題,將於後面說明
const fetchWeatherForecast = () => {
fetch(/*...*/)
.then((response) => response.json())
.then((data) => {
// ...
setWeatherElement({
description: weatherElements.Wx.parameterName,
weatherCode: weatherElements.Wx.parameterValue,
rainPossibility: weatherElements.PoP.parameterName,
comfortability: weatherElements.CI.parameterName,
});
});
};
  1. 由於在元件中多了舒適度(comfortability)和天氣描述代碼(weatherCode)的資料,因此記得在 useState() 的預設值中,也把這兩個屬性的預設值放進去:
const [weatherElement, setWeatherElement] = useState({
// ...
comfortability: '舒適至悶熱',
weatherCode: 0,
isLoading: true,
});
  1. 現在我們把這個寫好的方法,放到 useEffect 中去呼叫,像是這樣:
useEffect(() => {
console.log('execute function in useEffect');
fetchCurrentWeather();
fetchWeatherForecast();
}, []);

錯誤處理:留意 useState 的使用#

但是當我們這樣寫之後,你會看到「臺灣好天氣」中顯示溫度變成了 NaN,部分資料也無法正常顯示,表示資料出現了一些問題:

Imgur

為什麼會發生這樣的錯誤呢?

這並不新鮮,其實我們已經碰到過了,還記得之前我們有提到 setSomething 這個方法是會把舊有的資料全部清掉,用新的去覆蓋了,而這就是問題的原因。

因為我們呼叫了兩次不同的 API ,而且在裡面都各自使用了 setWeatherElement,但我們只把透過 API 取得的資料放進去,而沒有把舊有的資料保留下來。時好時壞是因為這兩道 API 回傳資料的速度每次並不一定,而最後取得資料的會把一開始 weatherElement 中的資料覆蓋掉。有時候 fetchCurrentWeather 比較快得到結果,有時候則是 fetchWeatherForecast 比較快,所以才會有不一致的情況。

要解決這個問題只需要把原本 state 的狀態再重新放入 setSomething 的方法中即可,還記得在 setSomething 這個方法中可以透過帶入函式來取得原有的資料狀態(prevState)嗎?這裡一樣可以透過這樣的方式,把在 weatherElement 中原有的狀態還去就可以了,寫法會像這樣:

const [weatherElement, setWeatherElement] = useState(/* ... */);
setWeatherElement((prevState) => {
// 記得要回傳新的資料狀態回去
return {
...prevState, // 保留原有的資料狀態
...newValue, // 添加或更新的資料
};
});

修改原本呼叫 setWeatherElement 的地方#

fetchCurrentWeatherfetchWeatherForecast 的這兩個函式中,都有使用到了 setWeatherElement 的方法,因此都需要記得把原本的狀態給帶進去:

  • setWeatherElement 中帶入函式,並在函式的參數中帶入 prevState 將可以取得原有的資料狀態
  • 透過物件的解構賦值把原有的資料放進去,後面再放入透過 API 取得的資料
  • 當箭頭函式單純只是要回傳物件時,可以連 return 都不寫,但回傳的物件需要使用小括號 () 包起來
  • 原本在 fetchCurrentWeather 的函式中,因為當時還沒辦法實際取得天氣描述和降雨機率,所以我們有先寫了假資料在它的 setWeatherElement 中,這裡要記得一併移除這兩個屬性
const fetchCurrentWeather = () => {
// ...
setWeatherElement((prevState) => ({
...prevState,
// description: '多雲時晴', // 移除這個屬性
// rainPossibility: 60, // 移除這個屬性
observationTime: locationData.time.obsTime,
locationName: locationData.locationName,
temperature: weatherElements.TEMP,
windSpeed: weatherElements.WDSD,
isLoading: false,
}));
};
const fetchWeatherForecast = () => {
// ...
setWeatherElement((prevState) => ({
...prevState,
description: weatherElements.Wx.parameterName,
weatherCode: weatherElements.Wx.parameterValue,
rainPossibility: weatherElements.PoP.parameterName,
comfortability: weatherElements.CI.parameterName,
}));
};

這時候畫面就能正確呈現了。

修改當使用者點擊重新整理時呼叫的方法#

現在在使用者初次載入頁面時,會同時呼叫到 fetchCurrentWeatherfetchWeatherForecast 這兩個方法,但在使用者點擊重新整理的時候還不會,因此在原本 <Refresh onClick={fetchCurrentWeather} /> 的地方,也要讓它能夠呼叫 fetchWeatherForecast,於是可以把程式碼改成:

<Refresh
onClick={() => {
fetchCurrentWeather();
fetchWeatherForecast();
}}
isLoading={isLoading}
>
最後觀測時間: {/* ... */}
</Refresh>

修改 weatherElement 的預設值#

現在你會發現,當頁面載入時,數字都會閃一下,因為它會先呈現我們在 useState 中的預設值,接著再拉取到中央氣象局的資料後,才把最新的資料帶入畫面中。現在既然我們已經有載入中的狀態,同時又可以取得最新的天氣資料,就可以把原本撰寫在 useState 中的預設值做個修改:

const [weatherElement, setWeatherElement] = useState({
observationTime: new Date(),
locationName: '',
temperature: 0,
windSpeed: 0,
description: '',
weatherCode: 0,
rainPossibility: 0,
comfortability: '',
isLoading: true,
});

顯示天氣描述與舒適度#

最後讓我們在 JSX 中把最新取得的 comfortability 的資料也呈現出來:

const App = () => {
// ...
const {
// ...
description,
comfortability,
} = weatherElement;
return (
{/* ... */}
<Description>
{description} {comfortability}
</Description>
{/* ... */}
);
};

換你了!取得天氣描述和降雨機率的資料#

在這個單元中,我們透過「一般天氣預報-今明 36 小時天氣預報」取得了「降雨機率」、「天氣描述」、「舒適度」與「天氣描述代碼」的資料。雖然現在已經能夠將資料正確顯示在畫面上,但程式碼還有可以改進的地方,我們將會在後續的單元中再來進行程式碼的重構。

現在要請你透過 fetch 取得資料,並整合到 App 元件的資料狀態中。同樣可以參考如下步驟:

  • 檢視「一般天氣預報-今明 36 小時天氣預報」API 中回傳的資料內容,找到「降雨機率」、「描述」與「舒適度」的欄位
  • 將原本透過 useState 取得的資料狀態改名為 weatherElementsetWeatherElement
  • 撰寫 fetchWeatherForecast 方法,在取得資料後使用 setWeatherElement 更新元件資料狀態
  • setWeatherElement 中使用函式以取得原本的資料狀態(prevState),並將此狀態保留在該物件中
  • onClick 中呼叫同時呼叫 fetchCurrentWeatherfetchWeatherForecast 方法
  • 修改 useState 中資料的預設值
  • 在 JSX 中顯示 descriptioncomfortability 的描述

本單元相關之網頁連結、完整程式碼與程式碼變更部分(時鐘圖示)可於 fetch-forecast-data 分支檢視:https://github.com/pjchender/learn-react-from-hook-realtime-weather-app/tree/fetch-forecast-data

Imgur