感謝 iT 邦幫忙與博碩文化,本系列文章已出版成書「從 Hooks 開始,讓你的網頁 React 起來」,首刷版稅將全額贊助 iT 邦幫忙鐵人賽,歡迎前往購書,鼓勵筆者撰寫更多優質文章。
昨天我們已經可以透過讓使用者點擊按鈕後來更新天氣資訊,今天就讓我們來看看怎麼樣可以在使用者一載入頁面的時候,就去取得最新的資料回來顯示。
要一載入頁面時就去執行某些行為會需要使用到 useEffect
這個 React Hooks。個人認為 useEffect
是在整個 React Hooks 中需要花最多時間去理解和消化的 Hook,其中很大一部分的原因也在於 useEffect
和過去傳統學習到的生命週期概念綁得很深,因此對於非初次學習 React 的開發者來說,在學習的時候會不自覺得想要把舊的思考模式套用到這個 useEffect
這個 Hook。
現在就讓我們來看一下 useEffect
這個 React Hook 最基本的用法。
你可以打開昨天在 CodeSandbox 上完成的即時天氣 App,開啟這個專案中的 ./src/WeatherApp.js
,我們先來試著感受一下 useEffect
:
react
中載入 useEffect
useEffect
這個方法的參數中需要帶入一個函式,而這個函式會在「畫面渲染完成」後被呼叫
console.log()
變更的程式碼如下:
// STEP 1:載入 useEffect
import React, { useState, useEffect } from 'react';
import styled from '@emotion/styled';
// ...
const WeatherApp = () => {
console.log('invoke function component');
// useState ...
// STEP 2:使用 useEffect Hook
useEffect(() => {
console.log('execute function in useEffect');
});
// handleClick ...
return (
<Container>
{console.log('render')}
<WeatherCard>
{/* ... */}
</WeatherCard>
</Container>
);
};
export default WeatherApp;
在三個不同的位置使用 console.log()
來看執行的時間點:
由於 useEffect
這個方法使用時需要在參數中帶入一個函式,因此透過 console.log('execute function in useEffect');
我們可以觀察這個函式被呼叫的時間點。
讓我們透過 CodeSandbox 中提供的 debug 網址來看一下:
從上圖中你會看到 console.log
出現的順序如下:
也就是說, 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
⚠️ 注意:請把這個段落看完後在實作,否則可能會進入無窮迴圈!
下圖是修改的部分:
存檔後來看一下結果:
糟糕了!怎麼陷入了無限迴圈!!!
我們先來了解一下為什麼會陷入無限迴圈。首先,當組件渲染完成後,會去執行 useEffect
中的函式,而這個函式中會去呼叫 fetchCurrentWeather
,在 fetchCurrentWether
向 API 請求完資料後會呼叫到 setCurrentWeather
,於是會促發組件重新渲染...,然後就繼續不斷這樣的循環...。
整個流程就像下面這樣的概念:
那麼要怎麼停止這個無限迴圈呢?
要停止這個無限迴圈會需要在「特定時間」讓 useEffect
內的函式不要被呼叫到就可以,這個「特定時間」通常是「已經向 API 拉取過資料」或者「React 內的資料沒有變動」時。
前面我們知道,useEffect
內的函式會在每一次畫面渲染完後被呼叫,好在 useEffect
還提供了第二個參數 dependencies
讓我們使用:
useEffect(<didUpdate>, [dependencies])
第二個參數稱作 dependencies
,它是一個陣列,只要每次重新渲染後 dependencies 內的元素沒有改變,任何 useEffect 裡面的函式就不會被執行!
所以 useEffect
內的函式會在組件渲染完成後被呼叫,現在多了一個前提:「組件渲染完後,如果 dependencies 有改變,才會呼叫 useEffect
內的 function」。具體來說是什麼意思呢?
現在回到原本的即時天氣 App 的程式碼,做一些修改:
useEffect
中帶入第二個參數,就帶入一個空陣列 []
就好console
加上 ---
這時候當我們重新整理頁面後,不會再出現無窮迴圈,而 console.log
的順序如下:
從上圖可以看到,這個組件被執行了兩次(有兩次 invoke function component
),為什麼會執行兩次呢?
如下圖,第一次畫面渲染後,因為 dependencies
的值才剛被帶入,所以會呼叫 useEffect
內的函式,並呼叫到 setCurrentWeather
這個方法,使得畫面再次渲染;第二次畫面渲染完後,發現 dependencies
陣列沒有改變(一樣什麼元素都沒有),因此就不會再次執行 useEffect
內的函式,也因此不會再次呼叫到 setCurrentWeather
,如此避免掉了無窮迴圈的問題:
在使用 useEffect
的時候大部分都會帶入這第二個 dependencies
參數,只是會根據需要在該陣列中放入元素。在今天的例子中,為了避免組件一直無窮更新的問題,因此會帶入一個空陣列,讓 useEffect
裡的這個函式只會被執行一次。
在有了 dependencies
的進入後,關於 useEffect
使用上最重要的一句就是:「組件渲染完後,如果 dependencies 有改變,才會呼叫 useEffect
內的 function」。
因為非常重要,所以請容我說三次:
組件渲染完後,如果 dependencies 有改變,才會呼叫 useEffect
內的 function
組件渲染完後,如果 dependencies 有改變,才會呼叫 useEffect
內的 function
組件渲染完後,如果 dependencies 有改變,才會呼叫 useEffect
內的 function
現在我們的即時天氣 App 已經可以在第一次載入畫面時,就去抓取最新的資料來呈現,今天完整的程式碼一樣放在 CodeSandbox 上可以檢視 Weather APP - fetch data with useEffect @ CodeSandbox
大家可能會好奇 useState
中的 state
指的是保存在 React 組件內部的資料狀態,那 useEffect
中的 effect
又是什麼呢?
這個 effect 指的是 副作用(side-effect) 的意思,在 React 中會把畫面渲染後和 React 本身無關而需要執行的動作稱做「副作用」,這些動作像是「發送 API 請求資料」、「手動更改 DOM 畫面」等等。
副作用(side-effect)又簡稱為 effect,所以就使用 useEffect
這個詞。因為 useEffect
內帶入的函式主要就是要用來處理這些副作用,因此這些帶入 useEffect
內的函式也會被稱作 effect
。
「手動更改 DOM 畫面」指的是透過瀏覽器原生的 API 或其他第三方套件去操作 DOM,而不是透過讓React 組件內
state
改變而更新畫面呈現的方式。
Weather APP - fetch data with useEffect @ CodeSandbox
好清楚,感謝分享.之前看過 useEffect 一直覺得不太清楚.現在清楚多了
有一個問題就是有看過
useEffect(() => {
// every times, like componentDidUpadet
console.log(`inside useEffect ${count}`)
return () => {
想請問這邊的生命週期是什麼呢
}
})
useEffect()
裡面的這個函式中如果回傳一個函式的話,這個函式主要是做一些「清理(cleanup)」的作用,這個 cleanup function 可以用來清理一些曾經被註冊過的事件。
假設我們先跳開過去生命週期的概念來看,它有兩個時間點會被執行:
回到生命週期的話,第一個時間點比較好理解,可以對應到 componentWillUnmount;第二個時間點則是因為在 React Hook 的 Function Component 中,這個 Function 每次都會重新被執行,所以才會需要在每次 component 重新渲染前被呼叫,這個時間點可以想成對應到 componentDidUpdate 的最前面會被呼叫。