5-4 頁面載入時就去請求資料 - useEffect 的基本使用

本單元對應的專案分支為:fetch-data-when-page-loaded-with-useEffect

單元核心#

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

  • 了解 useEffect 的使用
  • 了解 useEffect 中函式會被執行的時間點
  • 了解 useEffect 中 dependencies array 對其函式執行的影響
  • 透過 useEffect 讓頁面載入時即更新天氣資料

在上一個單元中,我們已經可以透過讓使用者點擊按鈕來更新天氣資訊,但實際上,比較好的做法應該是在使用者載入頁面的時候,就去取得最新的資料回來顯示;如果使用者想要看最新的資料,再按下重新整理的按鈕來更新資料。因此,現在就讓我們來看一下,要如何在頁面一載入時就去發送 API 請求拉取資料呢?

這裡我們會碰到本書以來的第二個 React Hooks,稱作 useEffect

個人認為 useEffect 是整個 React Hooks 中需要花最多時間去理解和消化的 Hook,其中很大部分原因在於 useEffect 和過去學習到的生命週期概念綁得很深,因此對於非初次學習 React 的開發者來說,學習的時候會不自覺想要把舊的思考模式套用到 useEffect 這個 Hook。

現在就讓我們來看一下 useEffect 這個 React Hook 最基本的用法。

useEffect 的基本使用#

  1. 先從 react 中載入 useEffect
// ./src/App.js
import React, { useState, useEffect } from 'react';
  1. 接著在 App 元件中試著使用 useEffect,useEffect 的參數中需要帶入一個函式,而這個函式會在「畫面轉譯完成」後被呼叫
// ./src/App.js
// ...
const App = () => {
const [currentTheme, setCurrentTheme] = useState('light');
const [currentWeather, setCurrentWeather] = useState({
/* ... */
});
// 加入 useEffect 方法,參數是需要放入函式
useEffect(() => {});
return {
/* ... */
};
};
  1. 最後我們在元件中的幾個不同位置使用 console.log() 看看
// ./src/App.js
// ...
const App = () => {
console.log('invoke function component'); // 元件一開始加入 console.log
const [currentTheme, setCurrentTheme] = useState('light');
const [currentWeather, setCurrentWeather] = useState({
/* ... */
});
useEffect(() => {
// useEffect 中 console.log
console.log('execute function in useEffect');
});
return (
<Container>
{/* JSX 中加入 console.log */}
{console.log('render')}
<WeatherCard>{/* ... */}</WeatherCard>
</Container>
);
};

在三個不同的位置使用 console.log() 來看執行的時間點:

Imgur

觀察 useEffect 中函式被執行的時間點#

由於 useEffect 這個方法使用時需要在參數中帶入一個函式,因此透過 console.log('execute function in useEffect'); 我們可以觀察這個函式被呼叫的時間點。

現在打開瀏覽器的開發者工具,在 console 面板中你應該可以看到 console.log 的訊息內容以如下的順序出現:

Imgur

tip

在 console 面板會看到有警告訊息顯示:「'setCurrentTheme' is assigned a value but never used」,這個訊息指的是在 App 元件中有定義了 setCurrentTheme 這個方法,但是沒有被使用。目前讀者可以先忽略這個訊息,在後面的單元中要自動切換亮/暗色主題時才會用這個方法。

關閉 React.StrictMode#

你會發現這裡雖然出現了兩次 invoke function component 和 render,但實際上我們只需要看方框標註的地方即可,前面多出來的兩次,主要是因為在 index.js 中,預設使用了 <React.StrictMode><App /> 包住,因此它會去多幫我們檢查元件的使用,進而多出了兩行。

這裡為了讓讀者對於 useEffect 觸發的時間點有更清楚的理解,讀者可以先把 <React.StrictMode> 拿掉:

// ./src/index.js
ReactDOM.render(<App />, document.getElementById('root'));

這時候同樣透過在瀏覽器的開發者工具中,你將只會看到如上圖方框標註中三行的內容:

圖一:Imgur

也就是說, useEffect 內的 function 會在元件轉譯完後被呼叫,要注意的是「轉譯完後」才會呼叫,如果你知道 callback function 的概念,這個 useEffect 內的函式就很像是元件轉譯完後要執行的 callback function。

跟著一起把這個重要的觀念重複唸一遍:元件轉譯完後才會呼叫 useEffect 內的 function

如果元件需要重新轉譯呢#

剛剛我們看到的是網頁重新整理後第一次載入網頁的情況,那如果使用到了 useState 提供的 setSomething 這個方法時,useEffect 中的函式會在什麼時候被呼叫呢?

你可以透過點擊右下角的「重新整理」按鈕來觸發元件更新。可以看到當我們使用 useState 提供的 setSomething 讓觸發畫面重新轉譯時,console.log 顯示的順序和剛剛第一次載入網頁時的順序是一樣的,因此,不管這個元件是第一次轉譯還是重新轉譯 useEffect 內的 function 一樣會在元件轉譯完後被呼叫。

Imgur

在第一次載入網頁時更新資料#

現在我們知道 useEffect 內的 function 會在元件轉譯完後被呼叫,這個時間點剛好非常適合來呼叫 API 並更新資料,於是,我們可以在 useEffect 中建立一個函式,並把拉取並更新元件資料的方法放進去(也就是 handleClick 的方法):

  1. 把原本的 handleClick 方法改名為 fetchCurrentWeather
  2. useEffect() 的函式中呼叫 fetchCurrentWeather
注意

請把這個段落看完後再實作,否則將會進入無窮迴圈!

Imgur

存檔後來看一下結果:

Imgur

糟糕了!你會發現 console 不斷噴出新東西,陷入了無限迴圈!!!

為什麼會陷入無限迴圈#

我們先來了解一下為什麼會陷入無限迴圈。

首先,當頁面第一次載入,元件轉譯完成後,會去執行 useEffect 中的函式,而這個函式中會在 fetchCurrentWeather 取得 API 回應的資料後,呼叫 setCurrentWeather 來更新畫面上的資料,更新畫面就表示該元件會重新轉譯,於是轉譯完後又會再次執行 useEffect 中的 fetchCurrentWeather 方法,接著再次呼叫 setCurrentWeather 觸發畫面重新轉譯,然後 useEffect 中的函式再次被呼叫,接著就繼續不斷這樣的循環......。

整個流程就像下面這樣的概念:

圖二Imgur

如何讓 useEffect 內的函式有條件的不被呼叫#

那麼要怎麼停止這個無限迴圈呢?

要停止這個無限迴圈會需要在「特定時間」讓 useEffect 內的函式不要被呼叫到就可以,這個「特定時間」通常是「已經向 API 拉取過資料」或者「React 內的資料沒有變動」時。

前面我們知道,useEffect 內的函式會在「每一次」畫面轉譯完後被呼叫,好在 useEffect 還提供了第二個參數 dependencies 讓我們使用:

useEffect(<didUpdate>, [dependencies])

第二個參數稱作 dependencies,它是一個陣列,只要每次重新轉譯後 dependencies 內的元素沒有改變,任何 useEffect 裡面的函式就不會被執行!

所以 useEffect 內的函式會在元件轉譯完成後被呼叫,現在多了一個前提:「元件轉譯完後,如果 dependencies 有改變,才會呼叫 useEffect 內的 function」。具體來說是什麼意思呢?

現在回到原本的「臺灣好天氣」的程式碼中,在 useEffect 中帶入第二個參數,帶入一個空陣列 []就好。帶入空陣列的話,因為空陣列中沒有元素,自然永遠都不會改變,因此就等同於只有在頁面載入時會執行 useEffect 中函式的內容:

// 第二個參數放入空陣列
useEffect(() => {
console.log('execute function in useEffect');
fetchCurrentWeather();
}, []);

這時候我們重新整理頁面,不會再出現無窮迴圈,而 console.log 的順序如下:

[元件初次轉譯]
invoke function component
render
execute function in useEffect
[因為 useEffect 中的 fetchCurrentWeather 函式中有呼叫了 setCurrentWeather,所以會再重新轉譯畫面]
invoke function component
render

我們可以看到,這個元件被執行了兩次(有兩次 invoke function component),為什麼會執行兩次呢?

如下圖,第一次畫面轉譯後,因為 dependencies 的值才剛被帶入,所以會呼叫 useEffect 內的函式,並呼叫到 setCurrentWeather 這個方法,使得畫面再次轉譯;第二次畫面轉譯完後,發現 dependencies 陣列沒有改變(一樣什麼元素都沒有),因此就不會再次執行 useEffect 內的函式,也因此不會再次呼叫到 setCurrentWeather,如此避免掉了無窮迴圈的問題:

圖三:

Imgur

在使用 useEffect 的時候大部分都會帶入這第二個 dependencies 參數,只是會根據需要在該陣列中放入不同元素。在今天的例子中,為了避免元件一直無窮更新的問題,因此會帶入一個空陣列,讓 useEffect 裡的這個函式只會被執行一次。

useEffect 中的 dependencies 陣列#

現在我們知道 useEffect 中函式執行的時間點一定會是元件轉譯完之後,至於這個函式到底會不會被呼叫則取決於 dependencies 陣列中的元素是否相同(Same-value equality)。

大家可以把 dependencies 陣列中放入的元素當作是「被觀察」的變數,你可以想像當我們把某個變數放入 dependencies 陣列中時,是在告訴這個 useEffect 說,幫我顧好這幾個變數喔!如果它們有改變的話,你就要再重新做一次事。

另外,這個有沒有改變的判斷,底層是用 Object.is() 這個方法來判斷,在大多數的情況下 Object.is()=== 的比較結果都是相同的,除了當值有可能是 -0NaN 這兩個情況,判斷方式才會有所不同。

因此,讀者們可以把這「相同」簡單想成是用 === 來比較,因此要特別留意的是,如果你是在 dependencies array 中放入「物件」或「函式」的話,即是兩個物件中的屬性和值完全相同,但因為物件或函式實際上參照到的是不同的記憶體位置,因此在比對時都會認為是不同的。

info

若對於物件與函式為什麼會參照到不同的記憶體位置,可以參考 Github 上單元說明頁中關於「談談 JavaScript 中 by reference 和 by value 的重要觀念」的連結。

useEffect 的 effect 指的是什麼#

另外,我們知道 useState 中的 state 指的是保存在 React 元件內部的資料狀態,那麼 useEffect 中的 effect 又是什麼呢?

這個 effect 指的是 副作用(side-effect) 的意思,在 React 中會把畫面轉譯後和 React 本身無關而需要執行的動作稱做「副作用」,這些動作像是「發送 API 請求資料」、「手動更改 DOM 畫面」等等。

副作用(side-effect)又簡稱為 effect,就使用 useEffect 這個詞,而 useEffect 內帶入的函式主要就是用來處理這些副作用,因此帶入 useEffect 內的函式也會被稱作 effect

note

「手動更改 DOM 畫面」指的是透過瀏覽器原生的 API 或其他第三方套件去操作 DOM,而不是透過讓 React 元件內 state 改變而更新畫面呈現的方式。

換你了!讓頁面一載入就自動更新資料吧#

現在要請你實際透過 useEffect 這個方法,當畫面載入時就自動拉取最新的觀測資料!現在,你可以參考下面了流程:

  • 關閉 index.js 中的 <React.StrictMode>
  • 在 App 元件中使用 useEffect 方法,並搭配 console.log 觀察 useEffect 中函式被執行的時間點
  • handleClick 方法改名為 fetchCurrentWeather
  • useEffect 的函式中呼叫 fetchCurrentWeather(可能會出現無窮迴圈)
  • 在 dependencies 陣列中放入空陣列 [],觀察 useEffect 中函式是否再次被呼叫

本單元相關之網頁連結、完整程式碼,以及程式碼變更部分可於 fetch-data-when-page-loaded-with-useEffect 分支檢視:https://github.com/pjchender/learn-react-from-hook-realtime-weather-app/tree/fetch-data-when-page-loaded-with-useEffect

Imgur


圖片中的文字內容:

圖一#

invoke function component:開始執行元件

render:轉譯元件

execute function in useEffect:執行 useEffect 內的函式

圖二#

畫面轉譯 useEffect 內的 fetchCurrentWeather 被執行 setCurrentWeather 被呼叫 畫面又重新轉譯

圖三#

畫面轉譯 第一次轉譯後 useEffect 內的 fetchCurrentWeather 被執行 setCurrentWeather 被呼叫 第二轉譯後 dependencies 沒有改變