Coding Hero

We solve your problems

Class component vs function component

2019-08-23 Frankreact

本篇介紹的是 React 的新 feature - hooks 與先前舊版本 class component 的差別。

React 在 16.8.6 之後,加入了新的 feature - hooks,對於開發方式產生了不小的變化,因為在 16.8.6 之前的版本裡,要使用內部狀態,或者要使用生命週期函數,一定得使用 class component 才能做到,而在 16.8.6 之後,除非必要,否則基本上已經不太需要寫 class component 了。

關於是否需要將既有的程式碼轉換成 hooks 的寫法,官方給的建議是,若開始寫新的程式碼,可以開始使用,而舊的 class component 程式碼,在升級版本成 16.8.6 以上的版本之後,仍然可以沿用,因此開發者可以自行考量維護與重構的時間成本與開發時程,再決定是否有翻新的需要。

我自己的想法是,行有餘力能升就升,因為 hooks 有它的強項是 class component 沒有的。

  1. useEffect 將最常用的生命週期函數寫法簡化,並且可以分門別類拆分開來擺放,對應到 design pattern 裡的 單一功能原則 是非常好的實踐。
  2. 可以自行組成 custom hooks,將抽象程式碼的複用程度盡可能地最大化。
  3. 理論上可以盡量將組件寫成純函式的形式,有利於寫單元測試。



以往我們在寫 class component 的時候,最常使用的生命週期函數是以下三個:

componentDidMount() {
  // 當元件載入完成到記憶體裡,並且即將render到畫面前的時間點
  // 適合用來執行初始值的設定,或者是向後端請求資料
  // 這個生命週期函數,只會載入一次!
}

componentDidUpdate() {
  // 當畫面有變動,需要根據畫面binding的資料改變,做出相對應的變化時使用
  // 例如下拉選單的selectedValue變動,需要重新和後端請求資料
}

componentWillUnmount() {
  // 當元件在畫面上用不到即將卸載時,需要執行一些clean up的動作的時間點
  // 例如 event listener或subscript的解除訂閱,
  // 未確實做好clean up的動作,有可能會造成記憶體洩漏
}

而我們在採用 hooks 的寫法後,則可以將上述三個生命週期函數合而為一,因為在先前 class component 的寫法中,通常在 componentDidMountcomponentDidUpdate 裡通常都會做很多一樣的事情,但 ComponentDidMount 只會執行一次,因此迫不得已必須要把重複的程式碼寫在 ComponentDidUpdate 裡,而在 16.8.6 之後,使用 hooks useEffect 的寫法,可以將三個生命週期函數簡化成以下:

useEffect(() => {
  // componentDidMount 與 componentDidUpdate的程式碼寫在此

  return () => {
    // componentWillUnmount的clean up方法寫在此
    // 就算你沒有要在這邊做事情,記得也請留空
  }
}, [dependency array])

唯一不一樣的地方是在 useEffect 中多了一個 dependecy array,這個矩陣的用意是告訴 React 有沒有必要執行 useEffect 裡面的程式碼區塊,若 dependency array 裡面的值沒有變化,裡面的程式碼就不會執行。範例如下:

useEffect(() => {
  fetchWeatherData(id)

  return () => {}
}, [fetchWeatherData, id])

我在上述範例程式碼做了一個 fetchWeatherData 的 function call,參數帶的是 id,並且告訴 React,當 feathWeatherData 與 id 沒有變化的話,就不需要再幫我執行裡面的程式,如此簡單。

在前面所提到 單一功能 原則的實踐,則可以將上述範例程式碼進一步重構為以下:

useWeatherData = (fetchWeatherData, id) => {
  useEffect(() => {
    fetchWeatherData(id)

    return () => {}
  }, [fetchWeatherData, id])
}

而 component 的全貌概觀如下:

import React, { useEffect } from "react"
import fetchWeatherData from "../somewhere" // 從別的地方引用進來的dependency

const MyWeatherComponent = ({ id }) => {
  useWeatherData(id)

  return <>{要 render 的內容}</>
}

const useWeatherData = (fetchWeatherData, id) => {
  useEffect(() => {
    fetchWeatherData(id)

    return () => {}
  }, [fetchWeatherData, id])
}

export default MyWeatherComponent

附帶一提,若要讓 useEffect 有如同 componentDidMount 的效果,可以這樣寫:

useEffect(() => {
  doSomething()

  return () => {}
  // 以下的dependency array若為空矩陣的話,形同componentDidMount
  // useEffect內的程式碼 - doSomething() 只會在組件載入時執行一次
}, [])

配合前面講到的 單一功能原則,若除了 fetchWeatherData 之外,還需要 call 另外一個 fetchCityData 的資料的話,以往 class component 的寫法如下:

...

componentDidMount() {
  // 單純的function call
  fetchWeatherData(id)
  fetchCityData()
}

...

componentDidMount 裡面就做了兩件事情,而使用 custom hooks 的 useEffect 寫法如下:

...

useWeatherData(id)
useCityData()

...

雖然寫法很類似,但是個人覺得賦予的意義不同,並且在 hooks 的 convention 裡 use 開頭隱含的就有 side effect 的意義在,因此在看程式碼馬上一目了然,知道這段是在做副作用的程式碼區塊。

綜合以上程式碼對 hooks 做一個初步的概觀,接下來的文章還會繼續提到 custom hooks 的使用法。