轉自 https://ithelp.ithome.com.tw/articles/10224270
--
昨天我們已經可以透過讓使用者點擊按鈕後來更新天氣資訊,今天就讓我們來看看怎麼樣可以在使用者一載入頁面的時候,就去取得最新的資料回來顯示。
要一載入頁面時就去執行某些行為會需要使用到 useEffect
這個 React Hooks。個人認為 useEffect
是在整個 React Hooks 中需要花最多時間去理解和消化的 Hook,其中很大一部分的原因也在於 useEffect
和過去傳統學習到的生命週期概念綁得很深,因此對於非初次學習 React 的開發者來說,在學習的時候會不自覺得想要把舊的思考模式套用到這個 useEffect
這個 Hook。
現在就讓我們來看一下 useEffect
這個 React Hook 最基本的用法。
useEffect 的基本使用
載入並使用 useEffect
你可以打開昨天在 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 中函式被執行的時間點
由於 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 內的函式有條件的不被呼叫
那麼要怎麼停止這個無限迴圈呢?
要停止這個無限迴圈會需要在「特定時間」讓 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
補充:useEffect 的 effect 指的是什麼
大家可能會好奇 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
參考資料
- Using the Effect Hook @ React Docs - Hooks
--