6-5 建立自己的鉤子 - Custom Hooks

本單元對應的專案分支為:custom-hooks

單元核心#

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

  • 了解如何將重複的邏輯整理成自己可使用的 Hooks

延續上一個單元專案程式碼的重構,現在重構後的 App.js 中,雖然已經比起原本的程式碼乾淨許多,基本上只做了拉取資料的動作,但因為 fetchCurrentWeatherfetchWeatherForecast 本身也做了不少事情,取得資料之後又要透過 setWeatherElement 把資料存到 React 元件中,我們有沒有什麼方式讓這個元件再更乾淨一些呢?

答案是肯定的,在 React 中,我們不只能夠使用 React 預先定義好的 Hooks,像是之前使用的 useStateuseEffectuseMemo 這些,還可以自己自訂 Hook。自訂的 Hook 可以幫我們把較複雜的程式邏輯抽到 Hook 內,並且可以在多個元件內重複使用外,甚至也可以打包起來,放到開源社群分享給有同樣需求的人使用。

現在就讓我們來看看怎麼樣定義自己的 Hook 吧!

Custom Hook 的概念#

自訂 Hook(Custom Hook)的概念其實很簡單,它和你之前寫的 React Component 基本上是一樣的,都是 JavaScript 的函式,而且在 Custom Hook 中一樣可以使用 useStateuseEffect 這些原本 React 就有提供的 Hooks,只是在 React Component 中最後你會回傳的是 JSX,而在 Hook 中最後回傳的是一些資料或改變資料的方法。此外,在自訂的 Hook 中,會遵循 React Hooks 的慣例,因此會使用 use 開頭來為該函式命名。

所以基本上你會建立 React Component 的話,就會自訂 Hook。另外,自訂的 Hook 一樣要遵守原本 React Hooks 的原則,像是 Hook 只能在 React 的 Functional Component 中使用(過去 React Component 除了函式之外,也可以用 class 建立)、Hook 不能放在迴圈或 if 判斷式內等等。

新增 useWeatherAPI 的 Hook#

現在就讓我們來建立一個名為 useWeatherAPI 的 Hook,在這個 Hook 中會幫助我們去向中央氣象局發送 API 請求,並且回傳取得的資料。

先在 ./src 資料夾中建立一個名為 hooks 資料夾,並新增一支名為 useWeatherAPI.js 的檔案,在裡面定義一個名為 useWeatherAPI 的函式,並透過 export 匯出:

Imgur

其實和建立 React Component 的步驟一樣吧!

定義 Custom Hook 內的功能#

接下來在 useWeatherAPI 這個函式中,就可以來向中央氣象局發送 API 請求天氣資料,這個部分因為先前都寫在 App.js 中了,因此把這個部分剪下貼上就好。

先把在 App.js 中定義的 fetchCurrentWeatherfetchWeatherForecast 這兩個函式剪下,貼到 useWeatherAPI.js 中:

// ./src/hooks/useWeatherAPI.js
const fetchCurrentWeather = () => {
/* ... */
};
const fetchWeatherForecast = () => {
/* ... */
};
const useWeatherAPI = () => {};
export default useWeatherAPI;

接著把原本寫在 App 元件中和拉取天氣資料有關的部分,搬到這個 useWeatherAPI 這個 Hook 內,其中包含:

  1. 匯入 react 套件提供的 useState, useEffect, useCallback 方法。在 Custom Hooks 中因為最後不會回傳 JSX,因此不需要匯入 react 套件提供的 React 物件
// ./src/hooks/useWeatherAPI.js
import { useState, useEffect, useCallback } from 'react';
  1. useState 中用來定義 weatherElement 的部分
// ./src/hooks/useWeatherAPI.js
const useWeatherAPI = () => {
// STEP 2:useState 中用來定義 weatherElement 的部分
const [weatherElement, setWeatherElement] = useState({
/* ... */
});
};
  1. 透過 useCallback 用來定義 fetchData() 的部分
// ./src/hooks/useWeatherAPI.js
const useWeatherAPI = () => {
const [weatherElement, setWeatherElement] = useState({
/* ... */
});
// STEP 3:透過 useCallback 用來定義 fetchData() 的部分
const fetchData = useCallback(async () => {
/* ... */
}, []);
};
  1. 透過 useEffect 用來呼叫 fetchData 的部分
const useWeatherAPI = () => {
const [weatherElement, setWeatherElement] = useState({
/* ... */
});
const fetchData = useCallback(async () => {
/* ... */
}, []);
// STEP 4:透過 useEffect 用來呼叫 fetchData 的部分
useEffect(() => {
fetchData();
}, [fetchData]);
};
  1. 最後一個步驟是和一般 React 元件最不同的地方,一般的 React 元件最終通常都是回傳 JSX,但在 Custom Hooks 中最後會 return 的是可以讓其他 React 元件使用的資料或方法。這就像當我們呼叫 useState 是會得到一個資料狀態和用來改變資料狀態的方法。所以這裡我們會回傳用來拉取資料的方法(fetchData)和拉取資料後取得的天氣資料(weatherElement
const useWeatherAPI = () => {
const [weatherElement, setWeatherElement] = useState({
/* ... */
});
const fetchData = useCallback(async () => {
/* ... */
}, []);
useEffect(() => {
fetchData();
}, [fetchData]);
// STEP 5:回傳要讓其他元件使用的資料或方法
return [weatherElement, fetchData];
};

現在我們就定義好了 useWeatherAPI 這個 Custom Hook。

讓 Custom Hook 可以接收參數#

Custom Hook 本質上也是個函式,所以它也可透過參數的方式取得資料。現在在 useWeatherAPI 中,我們還缺少幾個資料,像是 AUTHORIZATION_KEYLOCATION_NAMELOCATION_NAME_FORECAST,這些資料原本是放在 App 元件中,為了要讓 useWeatherAPI 也能取得這些資料,可以透過函式的參數把資料帶進來:

const useWeatherAPI = ({ locationName, cityName, authorizationKey }) => {
/* ... */
};

這裡可以看到用來取得即時天氣的 API(fetchCurrentWeather)需要帶入的是地區(即「臺北」);用來取得天氣預報的 API(fetchWeatherForecast)需要帶入的是縣市名稱(即「臺北市」),之所以會有這樣的差別,主要是因為中央氣象局在這兩道 API 需要的資料不同。為了讓變數的語意更清楚,我們把 LOCATION_NAME_FORECAST 改名為 cityName

現在進一步把這個變數帶入 fetchCurrentWeatherfetchWeatherForecast 中:

// ./src/hooks/useWeatherAPI.js
const fetchCurrentWeather = ({ authorizationKey, locationName }) => {
return fetch(
`https://opendata.cwb.gov.tw/api/v1/rest/datastore/O-A0003-001?
Authorization=${authorizationKey}&
locationName=${locationName}`
).then(/* ... */);
};
const fetchWeatherForecast = ({ authorizationKey, cityName }) => {
return fetch(
`https://opendata.cwb.gov.tw/api/v1/rest/datastore/F-C0032-001?
Authorization=${authorizationKey}&
locationName=${cityName}`
).then(/* ... */);
};

接著在 useWeatherAPI 的函式中,一樣透過參數把資料帶入這兩個方法:

  1. authorizationKey, locationName, cityName 傳到拉取 API 的方法中
  2. 在 useCallback 中要記得把變數放入 dependencies array 中,以確保這些資料改變時,能夠得到最新的 fetchData 方法
const useWeatherAPI = ({ locationName, cityName, authorizationKey }) => {
const [weatherElement, setWeatherElement] = useState({
/* ... */
});
const fetchData = useCallback(async () => {
// ...
const [currentWeather, weatherForecast] = await Promise.all([
// STEP 1:把 authorizationKey, locationName, cityName 傳到拉取 API 的方法中
fetchCurrentWeather({ authorizationKey, locationName }),
fetchWeatherForecast({ authorizationKey, cityName }),
]);
// ...
// STEP 2:在 useCallback 中要記得把變數放入 dependencies array 中
}, [authorizationKey, cityName, locationName]);
// ...
};

使用 Custom Hook#

當我們把拉取天氣資料的這一整個流程包成 Custom Hook 之後,在需要使用到天氣資料的 React 元件中,都可以透過它就可以取得中央氣象局回傳的資料。

使用方式非常簡單,就和使用其他的 React Hooks 一樣,現在就讓我們在 App.js 中來使用 useWeatherAPI

  1. 透過 import 載入 useWeatherAPI 這個 Custom Hook
  2. 直接呼叫 useWeatherAPI 後就能取得該 Hook 回傳的 weatherElementfetchData 方法
  3. 在呼叫 useWeatherAPI() 中,把它所需的參數 locationName, cityName, authorizationKey 放進去
  4. 整個使用方式是不是就和 useState 非常類似呢?
// ./src/App.js
// STEP 1 匯入 useWeatherAPI
import useWeatherAPI from './hooks/useWeatherAPI';
// ...
const App = () => {
// STEP 2:使用 useWeatherAPI
const [weatherElement, fetchData] = useWeatherAPI({
locationName: LOCATION_NAME,
cityName: LOCATION_NAME_FORECAST,
authorizationKey: AUTHORIZATION_KEY,
});
// ...
};

當啷~畫面又回來拉~

Imgur

基本上 Custom Hook 的定義和使用都不難,如果你會撰寫 React 中的 Functional Component,就一定會撰寫 Custom Hook。透過 Custom Hook ,可以幫助開發者將具有相同邏輯的功能統整在一個 Hook 中,方便重複使用這個函式的功用!

在這兩個單元中一口氣重構了不少程式碼,目的都是為了讓整個專案後續更容易維護,至於要怎麼判斷好不好維護,最簡單的方式是:「不求別人接手看得懂,只求自己一個月後打開程式還改得動」,如果現在寫的東西未來一個月後自己都看不懂的話,那肯定是不太好維護。

換你了!建立自己的 Hook#

Custom Hooks 在撰寫時要寫的是這個 Hook 帶入什麼樣的 input 後,將可以得到什麼樣的 output,與一般函式的思路蠻相近的。

現在要請你將與中央氣象局拉取資料有關的 API 拆分成一個獨立的 React Hook,想要使用這個 Hook 的人,只需要帶入 locationName, cityName, authorizationKey 之後,就可以取得對應的天氣資料。由於重構時程式碼的變動可能比較複雜,需要的時候都可以對應最下方的連結查看。實作時可以參考以下步驟:

  • ./src 資料夾中,建立名為 hooks 的資料夾,並在裡面新增 useWeatherAPI.js,在檔案中建立一個名為 useWeatherAPI 的函式
  • 把在 App.js 中定義的 fetchCurrentWeatherfetchWeatherForecast 這兩個函式剪下,貼到 useWeatherAPI.js
  • useWeatherAPI.js 中匯入 react 套件提供的 useState, useEffect, useCallback 方法
  • 把原本寫在 App 元件中和拉取天氣資料有關的部分可以搬到這個 useWeatherAPI 這個 Hook 內,其中包含 useState 中用來定義 weatherElement 的部分、透過 useCallback 用來定義 fetchData() 的部分、透過 useEffect 用來呼叫 fetchData 的部分
  • 把需要在 App 元件中使用到的函式和方法於 useWeatherAPI 回傳
  • 讓 useWeatherAPI 可以接收參數(locationName, cityName, authorizationKey
  • 在 App 元件中使用 useWeatherAPI 這個 Custom Hook

本單元相關之網頁連結、完整程式碼與程式碼變更部分可於 custom-hooks 分支檢視:https://github.com/pjchender/learn-react-from-hook-realtime-weather-app/tree/custom-hooks

Imgur