iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 18
3
Modern Web

從 Hooks 開始,讓你的網頁 React 起來系列 第 18

[Day 18 - 即時天氣] 拉取並呈現來自多道 API 的資料

感謝 iT 邦幫忙與博碩文化,本系列文章已出版成書「從 Hooks 開始,讓你的網頁 React 起來」,首刷版稅將全額贊助 iT 邦幫忙鐵人賽,歡迎前往購書,鼓勵筆者撰寫更多優質文章。

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

今天就讓我們來把需要的資料取回來,你一樣可以從昨天在 CodeSandbox 完成的 Weather APP - fetch data with useEffect 繼續,記得需要的話可以複製一份昨天的專案繼續做後面的修改。

找尋不足的資料

由於「降雨機率」這些資料很難用已經取得的那些資料去自己推估,所以勢必還是得要去找看看中央氣象局有沒有提供,如果中央氣象局沒有提供的話,就得要再去找其他第三方的服務了。

好在,回去再翻一下了中央氣象局提供的 API 後發現,在「預報」的地方可以取得降雨機率:

Imgur

實際透過這道 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)」:

Imgur

這真是太好了,我們不只拿到了降雨機率,連天氣現象和舒適度都拿到了。另外回傳的資料和文件中有提供天氣描述代碼,看起來我們還有機會透過這個代碼來顯示對應的天氣圖示:

Imgur

請求天氣預報資料

修改資料狀態的名稱

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

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

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

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

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

Imgur

撰寫 fetch 程式碼

現在回到專案中一樣可以透過 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() 中,就會看到回傳的結果:

Imgur

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

現在如同 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 中去呼叫,像是這樣:

Imgur

⚠️ 上面的做法仍不是完整的做法,會有錯誤產生,將於後面進行說明。

錯誤處理:留意 useState 的使用

但是當我們這樣寫之後,程式很快會發生錯誤:

Imgur

顯示時間的資料並不是有效格式,感覺實在有點納悶!不然我們先在 return 中把時間的部分註解掉,來看看會怎麼樣:

Imgur

現在畫面會出現了,但資料的呈現卻還是有點詭異:

Imgur

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

還記得在「Day 16 - 即時天氣 - 定義並請求組件會使用到的資料 - useState 的更多使用」的最後我們有提到,setSomething 這個方法是會把舊有的資料全部清掉,用新的去覆蓋掉,而這也就是這裡問題的原因。

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

要解決這個問題只需要記得把在 weatherElement 中原有的狀態還去就可以了,在 useStatesetSomething 這個方法中,除了可以直接帶入新的資料之外,也可以帶入一個函式,這個函式將可以取得前一次的資料狀態,寫法會像這樣:

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

// 在 setWeatherElement 中也可以帶入函式
// 可以透過這個函式的參數取得前一次的資料狀態
setWeatherElement((prevState => {
  // 記得要回傳新的資料狀態回去
  return {
    ...prevState            // 保留原有的資料狀態
    rainPossibility: 0.1    // 添加或更新的資料
  }
}))

套用到實際的專案中會像這樣:

  • setWeatherElement 中帶入函式,並在函式的參數中帶入 prevState 將可以取得原有的資料狀態
  • 透過物件的解構賦值把原有的資料放進去,後面在放入透過 API 取得的資料
  • 當箭頭函式單純只是要回傳物件時,可以連 return 都不寫,但回傳的物件需要使用小括號 () 包起來

Imgur

這時候畫面就能夠正確呈現了,最後我們也一併更新一開始使用 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
  • 記得把剛剛註解掉的「最後觀測時間」打開

Imgur

到目前為止還剩下「天氣圖示」的地方還沒完成第一版就差不多大功告成拉!但現在這個程式碼仍然不是最佳的寫法,我們會在後面幾天繼續優化與說明。

今天完整的程式碼一樣可以在 CodeSandbox 的 Weather APP - fetch multiple data 檢視。

程式範例

Weather APP - fetch multiple data @ CodeSandbox

參考資料


上一篇
[Day 17 - 即時天氣] 頁面載入時就去請求資料 - useEffect 的基本使用
下一篇
[Day 19 - 即時天氣] 在 useEffect 中定義並使用 async 函式
系列文
從 Hooks 開始,讓你的網頁 React 起來30

1 則留言

1
johnny25678
iT邦新手 5 級 ‧ 2021-01-08 10:32:51

Hi, 請問當我使用兩次fetch api時,就算用了prevState,會不會有這樣的狀況:

第一個fetch到了 -> 用之前的prevState去setState -> 還沒setState完成,第二個fetch到了 -> 用一樣的prevState去setState,結果prevState不是最新的

我想我的問題應該是setState是非同步的嗎?

pjchender iT邦新手 4 級 ‧ 2021-01-19 11:38:55 檢舉

setState 是非同步的,如果是在 setState() 的參數中使用 function,用 prevState 他一定會拿到前一次最新的狀態,但如果你不是在 setState 中用帶入 function 的方式取得 prevState,那麼就有可能會出現你說的情況。

我要留言

立即登入留言