Coding Hero

We solve your problems

React hooks 簡介

2019-10-12 Frankreact

本篇要介紹的是 React 最常見的三個 hooks 的基本使用方法。

  • useState
  • useEffect
  • useContext


在正文開始前,先介紹一個 VSCode 套件 - ES7 React/Redux/GraphQL/React-Native snippets,這個套件內含很多寫 React 常會使用到的 code snippet,新功能的 hooks 相關 snippet 也已經包含在此套件裡,相當好用方便,可節省大幅打字時間,建議安裝。

vscode react snippets plugin demo example

  • 如上所示,我先在畫面上敲了 rafc → 按下 tab 鍵,VSCode 會自動幫忙生成與檔名相對應的 component (註:rafc = react arrow function component)
  • 接著我使用了 useState 的 code snippet,VSCode 會自動幫你選取好要輸入名稱的區間,按下 tab 之後會跳到初始值的設定,再按一下 tab 鍵就設定完成,並且把定義好的常數直接變成 lower camel case 的型式。


useState


useState 功能如同 class component 裡的 this.state,可以想像成一個有著 getter/setter 的 array 封裝,概觀如下:

const [state, setState] = useState("default value")
// default value可為任意型別的任意值

與 class component 的 state 不同的是,useState 的型別是 array,因此在解構的時候,順序不能錯,getter 在前面,setter 在後面,而初始值的型別則不限定。

useEffect


主要用來替代 class component 的生命週期函數 - componentDidMountcomponentDidUpatecomponentWillUnmount,詳細一點的介紹與解釋可以看 Class component vs function component 的內容。

useContext


React 舊版的 context api 的進化版,主要用途是用來讓組件間的狀態可以共享,我們都知道,React 傳遞 props 的順序是由父 → 子 → 孫的方式由上往下傳遞,若在父組件有個 state 要往下傳遞到孫組件,偏偏子組件用不到的話,子組件還是得做個資料的傳遞者,傳遞這個對它自己本身毫無用處的參數,這樣的架構會容易導致程式碼變得混亂難以閱讀。

以實務經驗來看的話,資料連續傳遞個三層,過個兩三週再回來看程式碼,可能已經有點忘記當初裡面寫的東西是什麼了,看到這種恐怖的東西出現,就算是自己寫的,不禁也會讓人想問,這真的確定是我本人寫的嗎?為何我自己完全看不懂?🤨

doubt

之前我們公司有個案子,在 table view 呈現的每列資料裡要包含很多的功能動作,例如簽呈、送審、轉往下一關,我們做了按鈕出來,按下之後會打開一個 modal,輸入該填的資料後按下送出,會將資料狀態改變並往下一個審核關卡送。

由於 modal 的開窗功能都是類似的,因此當時我們做了一個 wrapper component 來共用開窗的程式碼,並且在 modal wrapper 的 children 裡面塞入要 render 的表單畫面與執行送出的按鈕

當時的規劃是把主要需要共用的狀態與資料都寫在跟 table view 同層的位置,當有需要的話就往下傳遞,所以這個功能模組的架構大概是這樣:

  • 父 (table view component) → 子 (modal wrapper) → 孫 (執行動作的表單 component)

But…就是這個 but,看到這裡有沒有發現這個實際案例跟前述所說相當類似?modal 只是負責 props 的轉送與它自己要做的事情,更不要說在孫 component 裡的程式碼還有可能再重構拆 component 的情形了。

當 table view component 把 state 與 event handler 傳給 modal 時,modal 已經因為 code reuse 的關係而抽象化,所以當時我們傳給 modal 的 props 統一命名用 data/callback 之類的參數再往下傳遞。

由於非常多的 component 都依賴 modal wrapper component,而 modal wrapper 底下的孫元件在執行送出的動作時,一大堆相同名稱的 callback() 呼叫,在經過了層層傳遞後,已經完全不知道這個所謂的 callback 是由誰透過 modal 傳遞過來的…在找問題時花了非常多的時間,事後也是學個教訓,大家也是在摸索中成長,之後也才漸漸了解到比較適合的解法是使用 context + HOC 或合適的 global state 管理方式如 Redux

註一:上述經典問題叫做 props drilling,觀念參考可以看 👉這篇文章 👈
註二:HOC 的部分會再另開篇幅講
註三:其實這個案子已經是使用 Redux,只是當時覺得 modal 功能只是小東西因此沒有把狀態放到 Redux 裡,才導致此問題

Context 起手式: Provider + Consumer

簡單記住兩個口訣:

  • Provider 提供內容
  • Consumer 消化內容

將要提供的公用資料放到 Provider 裡,在被 Provider 包裹的子組件皆可以當作消費者(消化資料的人),取得 Provider 提供的資料內容。

示例如下:

👉 建立 Context 的 instance


// SharedDataContext.js
import { createContext } from "react"

const SharedDataContext = createContext(null)

export default SharedDataContext

👉 將需要共用 隔空抓藥 🙈 的狀態放到 Provider 的 value 裡:


// Container.js
import React, { useState } from "react"
import ChatRoom from "./ChatRoom"
import MessageList from "./MessageList"
import SharedDataContext from "./SharedDataContext"

const Container = () => {
  const [chatRoomId, setChatRoomId] = useState("test chatRoomId")
  const [chatUser, setChatUser] = useState("chat user id")

  return (
    <SharedDataContext.Provider
      value={{
        chatRoomId,
        setChatRoomId,
        chatUser,
        setChatUser
      }}
    >
      <ChatRoom>
        <MessageList />
      </ChatRoom>
    </SharedDataContext.Provider>
  )
}

這邊要稍微注意 SharedDataContext 一定要大寫,而非 javascript 習慣的 low camel case 寫法 sharedDataContext,因為我們要把它當成 component 來使用,jsx 的語法 component 第一個字母一定要大寫。

👉 此時我們要在孫組件 - MessageList 取用 Context,簡單示意如下:


// MessageList.js
import React, { useContext } from "react"
import SharedDataContext from "./SharedDataContext"

const MessageList = () => {
// 將需要取用的資料解構出來:
const { chatRoomId, setChatRoomId, setChatUser } = useContext(SharedDataContext)

...

// Do something....

...

return <div>I am MessageList</div>
}

結語


此三個 hooks 可以說是能見度最高、最常使用的 hook,請一定要熟悉它們的用法。