iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 17
9
Modern Web

從 Hooks 開始,讓你的網頁 React 起來系列 第 17

[Day 17 - 即時天氣] 頁面載入時就去請求資料 - useEffect 的基本使用

感謝 iT 邦幫忙與博碩文化,本系列文章已出版成書「從 Hooks 開始,讓你的網頁 React 起來」,首刷版稅將全額贊助 iT 邦幫忙鐵人賽,歡迎前往購書,鼓勵筆者撰寫更多優質文章。

昨天我們已經可以透過讓使用者點擊按鈕後來更新天氣資訊,今天就讓我們來看看怎麼樣可以在使用者一載入頁面的時候,就去取得最新的資料回來顯示。

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

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

useEffect 的基本使用

載入並使用 useEffect

你可以打開昨天在 CodeSandbox 上完成的即時天氣 App,開啟這個專案中的 ./src/WeatherApp.js,我們先來試著感受一下 useEffect

  1. 先從 react 中載入 useEffect
  2. useEffect 這個方法的參數中需要帶入一個函式,而這個函式會在「畫面渲染完成」後被呼叫
  3. 在不同位置使用 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() 來看執行的時間點:

Imgur

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

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

讓我們透過 CodeSandbox 中提供的 debug 網址來看一下:

Imgur

從上圖中你會看到 console.log 出現的順序如下:

Imgur

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

跟著一起把這個重要的觀念重複唸一遍:

組件渲染完後才會呼叫 useEffect 內的 function

如果組件有重新渲染呢?

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

讓我們來透過點擊右下角的「重新整理」按鈕來觸發組件更新:

Imgur

你可以看到當我們使用 useState 提供的 setSomething 讓觸發畫面重新渲染時,console.log 顯示的順序和剛剛第一次載入網頁時的順序是一樣的,因此,不管這個組件是第一次渲染還是重新渲染 useEffect 內的 function 一樣會在組件渲染完後被呼叫。

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

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

  1. handleClick 這個方法改名為 fetchCurrentWeather
  2. useEffect() 的函式中呼叫 fetchCurrentWeather

⚠️ 注意:請把這個段落看完後在實作,否則可能會進入無窮迴圈!

下圖是修改的部分:

Imgur

存檔後來看一下結果:

Imgur

糟糕了!怎麼陷入了無限迴圈!!!

為什麼會陷入無限迴圈

我們先來了解一下為什麼會陷入無限迴圈。首先,當組件渲染完成後,會去執行 useEffect 中的函式,而這個函式中會去呼叫 fetchCurrentWeather,在 fetchCurrentWether 向 API 請求完資料後會呼叫到 setCurrentWeather,於是會促發組件重新渲染...,然後就繼續不斷這樣的循環...。

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

Imgur

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

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

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

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

useEffect(<didUpdate>, [dependencies])

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

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

現在回到原本的即時天氣 App 的程式碼,做一些修改:

  1. useEffect 中帶入第二個參數,就帶入一個空陣列 []就好
  2. 為了方便區隔,把組件被呼叫後的第一次 console 加上 ---

Imgur

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

Imgur

從上圖可以看到,這個組件被執行了兩次(有兩次 invoke function component),為什麼會執行兩次呢?

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

Imgur

在使用 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

參考資料


上一篇
[Day 16 - 即時天氣] 定義並請求組件會使用到的資料 - useState 的更多使用
下一篇
[Day 18 - 即時天氣] 拉取並呈現來自多道 API 的資料
系列文
從 Hooks 開始,讓你的網頁 React 起來30

1 則留言

1
hannahpun
iT邦新手 5 級 ‧ 2019-11-01 08:43:48

好清楚,感謝分享.之前看過 useEffect 一直覺得不太清楚.現在清楚多了
有一個問題就是有看過

useEffect(() => {
      // every times, like componentDidUpadet
      console.log(`inside useEffect ${count}`)
	  return () => {
          想請問這邊的生命週期是什麼呢
      }
  })
pjchender iT邦新手 4 級 ‧ 2019-11-05 11:16:57 檢舉

useEffect() 裡面的這個函式中如果回傳一個函式的話,這個函式主要是做一些「清理(cleanup)」的作用,這個 cleanup function 可以用來清理一些曾經被註冊過的事件。

假設我們先跳開過去生命週期的概念來看,它有兩個時間點會被執行:

  1. cleanup function 會在該 component 即將從畫面離開前被呼叫到
  2. cleanup function 會在每次 component 要重新渲染前被呼叫

回到生命週期的話,第一個時間點比較好理解,可以對應到 componentWillUnmount;第二個時間點則是因為在 React Hook 的 Function Component 中,這個 Function 每次都會重新被執行,所以才會需要在每次 component 重新渲染前被呼叫,這個時間點可以想成對應到 componentDidUpdate 的最前面會被呼叫。

我要留言

立即登入留言