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 的基本使用#
- 先從
react中載入useEffect
- 接著在 App 元件中試著使用
useEffect,useEffect 的參數中需要帶入一個函式,而這個函式會在「畫面轉譯完成」後被呼叫
- 最後我們在元件中的幾個不同位置使用
console.log()看看
在三個不同的位置使用 console.log() 來看執行的時間點:

觀察 useEffect 中函式被執行的時間點#
由於 useEffect 這個方法使用時需要在參數中帶入一個函式,因此透過 console.log('execute function in useEffect'); 我們可以觀察這個函式被呼叫的時間點。
現在打開瀏覽器的開發者工具,在 console 面板中你應該可以看到 console.log 的訊息內容以如下的順序出現:

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> 拿掉:
這時候同樣透過在瀏覽器的開發者工具中,你將只會看到如上圖方框標註中三行的內容:
圖一:
也就是說, useEffect 內的 function 會在元件轉譯完後被呼叫,要注意的是「轉譯完後」才會呼叫,如果你知道 callback function 的概念,這個 useEffect 內的函式就很像是元件轉譯完後要執行的 callback function。
跟著一起把這個重要的觀念重複唸一遍:元件轉譯完後才會呼叫 useEffect 內的 function。
如果元件需要重新轉譯呢#
剛剛我們看到的是網頁重新整理後第一次載入網頁的情況,那如果使用到了 useState 提供的 setSomething 這個方法時,useEffect 中的函式會在什麼時候被呼叫呢?
你可以透過點擊右下角的「重新整理」按鈕來觸發元件更新。可以看到當我們使用 useState 提供的 setSomething 讓觸發畫面重新轉譯時,console.log 顯示的順序和剛剛第一次載入網頁時的順序是一樣的,因此,不管這個元件是第一次轉譯還是重新轉譯 useEffect 內的 function 一樣會在元件轉譯完後被呼叫。

在第一次載入網頁時更新資料#
現在我們知道 useEffect 內的 function 會在元件轉譯完後被呼叫,這個時間點剛好非常適合來呼叫 API 並更新資料,於是,我們可以在 useEffect 中建立一個函式,並把拉取並更新元件資料的方法放進去(也就是 handleClick 的方法):
- 把原本的
handleClick方法改名為fetchCurrentWeather - 在
useEffect()的函式中呼叫fetchCurrentWeather
注意
請把這個段落看完後再實作,否則將會進入無窮迴圈!

存檔後來看一下結果:

糟糕了!你會發現 console 不斷噴出新東西,陷入了無限迴圈!!!
為什麼會陷入無限迴圈#
我們先來了解一下為什麼會陷入無限迴圈。
首先,當頁面第一次載入,元件轉譯完成後,會去執行 useEffect 中的函式,而這個函式中會在 fetchCurrentWeather 取得 API 回應的資料後,呼叫 setCurrentWeather 來更新畫面上的資料,更新畫面就表示該元件會重新轉譯,於是轉譯完後又會再次執行 useEffect 中的 fetchCurrentWeather 方法,接著再次呼叫 setCurrentWeather 觸發畫面重新轉譯,然後 useEffect 中的函式再次被呼叫,接著就繼續不斷這樣的循環......。
整個流程就像下面這樣的概念:
圖二
如何讓 useEffect 內的函式有條件的不被呼叫#
那麼要怎麼停止這個無限迴圈呢?
要停止這個無限迴圈會需要在「特定時間」讓 useEffect 內的函式不要被呼叫到就可以,這個「特定時間」通常是「已經向 API 拉取過資料」或者「React 內的資料沒有變動」時。
前面我們知道,useEffect 內的函式會在「每一次」畫面轉譯完後被呼叫,好在 useEffect 還提供了第二個參數 dependencies 讓我們使用:
第二個參數稱作 dependencies,它是一個陣列,只要每次重新轉譯後 dependencies 內的元素沒有改變,任何 useEffect 裡面的函式就不會被執行!
所以 useEffect 內的函式會在元件轉譯完成後被呼叫,現在多了一個前提:「元件轉譯完後,如果 dependencies 有改變,才會呼叫 useEffect 內的 function」。具體來說是什麼意思呢?
現在回到原本的「臺灣好天氣」的程式碼中,在 useEffect 中帶入第二個參數,帶入一個空陣列 []就好。帶入空陣列的話,因為空陣列中沒有元素,自然永遠都不會改變,因此就等同於只有在頁面載入時會執行 useEffect 中函式的內容:
這時候我們重新整理頁面,不會再出現無窮迴圈,而 console.log 的順序如下:
我們可以看到,這個元件被執行了兩次(有兩次 invoke function component),為什麼會執行兩次呢?
如下圖,第一次畫面轉譯後,因為 dependencies 的值才剛被帶入,所以會呼叫 useEffect 內的函式,並呼叫到 setCurrentWeather 這個方法,使得畫面再次轉譯;第二次畫面轉譯完後,發現 dependencies 陣列沒有改變(一樣什麼元素都沒有),因此就不會再次執行 useEffect 內的函式,也因此不會再次呼叫到 setCurrentWeather,如此避免掉了無窮迴圈的問題:
圖三:

在使用 useEffect 的時候大部分都會帶入這第二個 dependencies 參數,只是會根據需要在該陣列中放入不同元素。在今天的例子中,為了避免元件一直無窮更新的問題,因此會帶入一個空陣列,讓 useEffect 裡的這個函式只會被執行一次。
useEffect 中的 dependencies 陣列#
現在我們知道 useEffect 中函式執行的時間點一定會是元件轉譯完之後,至於這個函式到底會不會被呼叫則取決於 dependencies 陣列中的元素是否相同(Same-value equality)。
大家可以把 dependencies 陣列中放入的元素當作是「被觀察」的變數,你可以想像當我們把某個變數放入 dependencies 陣列中時,是在告訴這個 useEffect 說,幫我顧好這幾個變數喔!如果它們有改變的話,你就要再重新做一次事。
另外,這個有沒有改變的判斷,底層是用 Object.is() 這個方法來判斷,在大多數的情況下 Object.is() 和 === 的比較結果都是相同的,除了當值有可能是 -0 或 NaN 這兩個情況,判斷方式才會有所不同。
因此,讀者們可以把這「相同」簡單想成是用 === 來比較,因此要特別留意的是,如果你是在 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

圖片中的文字內容:
圖一#
invoke function component:開始執行元件
render:轉譯元件
execute function in useEffect:執行 useEffect 內的函式
圖二#
畫面轉譯 useEffect 內的 fetchCurrentWeather 被執行 setCurrentWeather 被呼叫 畫面又重新轉譯
圖三#
畫面轉譯 第一次轉譯後 useEffect 內的 fetchCurrentWeather 被執行 setCurrentWeather 被呼叫 第二轉譯後 dependencies 沒有改變