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 沒有改變