iT邦幫忙

2021 iThome 鐵人賽

DAY 9
0
Modern Web

用React刻自己的投資Dashboard系列 第 9

用React刻自己的投資Dashboard Day9 - useEffect hook

tags: 2021鐵人賽 React

既上一篇介紹完useState hook後,本篇就來介紹Day6也有用到的useEffect hook,在React官網有提到,如果使用者熟悉React class component的生命週期,那麼可以用下面這個提示來理解useEffect:

如果你熟悉 React class 的生命週期方法,你可以把 useEffect 視為 componentDidMount,componentDidUpdate 和 componentWillUnmount 的組合。

從下面這張React官方提供圖可以看出React生命週期的三個階段,Mounting(創建)、Updating(更新)、Unmounting(銷毀),不過要完整地解釋這張圖會需要比較多的篇幅,這邊只要先了解執行順序為先是componentDidMount,再來是componentDidUpdate,最後是componentWillUnmount,再來就直接看官網的範例即可。

Class 與 useEffect用法差異

要知道用法上的差異,可以從React官網上的範例來理解,這個範例是用React去寫一個計數器,並且在React對DOM進行變更後立即更新網頁標題,下面就分別用class與hook去寫,藉由範例認識其差異。

先將相同功能的程式碼比較一下,使用hook的程式碼比較短一些,也發現class版本需要用到componentDidMount與componentDidUpdate去實現這樣的功能,而hook版本只需要一個useEffect就可以處理,當然並不是程式碼短就是好,因此接下來分析一下功能差別。

class版本需要用到兩個method的原因

因為我們想要在一進入網頁的時候,就讓網頁的title顯示"You clicked 0 times",因此需要在Mounting的時候呼叫一次,之後在點擊按鈕的時候,count這個state會被更新,為了要將網頁的title也更新為"You clicked n times",因此需要呼叫componentDidUpdate。

可能有人會說,難道沒有一個method可以讓React在mounting及updating都去執行這行程式碼嗎?可惜React的class component剛好就是沒有這個功能,因此就需要寫兩次。

將上圖中的class版本程式碼放大如下:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }
  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

為什麼useEffect只要一行就可以

相對於class版本的程式碼,hook版本只寫了一行useEffect就達成這個範例的要求,原因是useEffect會在每次重新render之後都執行一次,不管這個render是在mounting階段或updating階段所執行的,useEffect都會執行,因此當state變動,自然會重新render,也就啟動useEffect內的程式,網頁的title也就更新了,真的是非常方便。

然後還有一個小地方是,useEffect需要在component內呼叫,才能去抓到最新的state,這個是javascript closures的特點,而React巧妙地運用了這點。

將上圖中的hook版本程式碼放大如下:

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

回到Day6的Card.js來看看

在Card.js中,因為我希望在以下時間點去觸發抓取數據的程式:

  1. 使用者進入網頁時,需先載入視窗範圍內可看見的圖 (Mounting Card Component)
  2. 使用者scroll down時,動態載入新的圖 (Mounting Card Component)
  3. 使用者按下更新按鈕,需重新抓取數據更新圖表 (Updating Card Component)

從上述需求可以發現,抓取數據這個動作,會在Mounting及Updating階段執行,因此useEffect就符合這樣的需求。所以在Card.js內寫了一個fetchData函數,並在useEffect內呼叫它,即完成這個功能。

Card.js

const Card = (props) => {
  // 定義state
  const [chartOption, setChartOption] = useState({...});
  
  // 抓取API數據
  const fetchData = (series_id) => {...};
  
  // effect
  useEffect(() => {
    fetchData(props.item.series_id);
  }, []);
  
  return (
    ...
  )
}

無限迴圈的陷阱

在上面的程式碼可以看到useEffect內有一個空的陣列,它的功能是避免程式陷入無限迴圈中,原因如下圖,在mount時執行render,完成之後effect就會啟動,因此就變更了state,React發現state改變後又重新render,再來又觸發effect,因此就形成了一個無限迴圈。

這個無限迴圈的問題,可以透過useEffect的dependencies來解決,例如官網的範例如下:

如果count沒有改變,effect就不會執行。

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes

因此我在Card的useEffect放入一個空陣列當作dependencies,因為它一直都是空的,所以react只會執行一次effect,雖然這不是最正確的方式,不過算是暫時解決這個問題。

小結

useEffect真的是有點複雜的功能,不過還蠻好用的,在這篇提到的無限迴圈問題還沒結束,下一篇要來研究到底useEffect中的dependencies到底要放一些什麼,同時也會提到useCallback這個hook。


上一篇
用React刻自己的投資Dashboard Day8 - useState hook
下一篇
用React刻自己的投資Dashboard Day10 - 用useCallback hook幫你記住函式
系列文
用React刻自己的投資Dashboard30

尚未有邦友留言

立即登入留言