iT邦幫忙

2024 iThome 鐵人賽

DAY 21
0

https://ithelp.ithome.com.tw/upload/images/20241005/20168201I9aukIm6Vq.png

今天要介紹的是 React 的 Hook 模式,嚴格來說 Hooks 不一定是一種設計模式,但它可以替代很多傳統的設計模式。
介紹 Hooks 前,先來看看 React class component 以及它的一些缺點。

Class Component

在一個 class component 中,會包含該元件的狀態、生命週期方法和客製化方法,示意程式碼如下:

class MyComponent extends React.Component {
  /* 定義元件狀態,綁定客製化方法 */
  constructor() {
    super()
    this.state = { ... }

    this.customMethodOne = this.customMethodOne.bind(this)
    this.customMethodTwo = this.customMethodTwo.bind(this)
  }

  /* 生命週期方法 */
  componentDidMount() { ...}
  componentWillUnmount() { ... }

  /* 客製化方法 */
  customMethodOne() { ... }
  customMethodTwo() { ... }

  /* 要渲染到畫面的 element */
  render() { return { ... }}
}

class component 有下列幾項缺點:

增加狀態邏輯時,需花心力重構程式碼

如果我們原有一個單純的按鈕元件如下:

function Button() {
  return <div className="btn">disabled</div>;
}

一切運作都沒問題,但如果需要為按鈕加上狀態切換的邏輯,當使用者點擊按鈕後切換 enabled 的狀態,那就需要改寫為 class component 來定義和處理元件狀態,改寫後如下:

export default class Button extends React.Component {
  constructor() {
    super();
    this.state = { enabled: false };
  }

  render() {
    const { enabled } = this.state;
    const btnText = enabled ? "enabled" : "disabled";

    return (
      <div
        className={`btn enabled-${enabled}`}
        onClick={() => this.setState({ enabled: !enabled })}
      >
        {btnText}
      </div>
    );
  }
}

在改寫過程中,開發者需要熟悉 JavaScript class 和 this 運作原理,以免出錯,因此要耗費較多心力來重構,對開發者心智負擔較大。

以 HOC 或 Render Prop 共用邏輯會增加複雜性

另外,如果想要在多個 class component 間共用邏輯,可使用 HOC 或 Render Prop 模式,但若要使用這些模式,可能又要重構 component 程式碼,無法輕易加上共用邏輯,且當共享邏輯變多時,可能會形成多層的包裝元件,導致「包裝地獄」如下,包裝地獄會讓資料流變得難以理解,也相對更難除錯,降低程式碼的可讀性與可維護性。

// 包裝地獄
<WrapperOne>
  <WrapperTwo>
    <WrapperThree>
      <WrapperFour>
        <WrapperFive>
          <Component>
            <h1>Finally in the component!</h1>
          </Component>
        </WrapperFive>
      </WrapperFour>
    </WrapperThree>
  </WrapperTwo>
</WrapperOne>

邏輯散落在各生命週期方法內,難以維護

當邏輯變多時,class component 的大小會逐漸增加,程式碼逐漸變得冗長,且邏輯會散落在各生命週期方法內,以一個要處理訂閱文章的副作用邏輯來說,class component 需要透過 3 個生命週期方法才能處理好副作用:
(關於副作用處理的更多說明,詳情請見[React] React 中的副作用處理、初探 useEffect)

componentDidMount(){
   //mount 後進行副作用處理
    articleAPI.subscribeUpdates(
        this.props.id,
        this.handleArticleUpdate
    )
}

componentDidUpdate(){
    //取消訂閱前一次 render 版本的 props.id 的文章
    articleAPI.unsubscribeUpdates(
        prevProps.id,
        this.handleArticleUpdate
    );
    
    //訂閱這次 render 的 props.id 的文章
    articleAPI.subscribeUpdates(
        this.props.id,
        this.handleArticleUpdate
    );
}

componentWillUnmount(){
    //清除副作用的影響
    articleAPI.unsubscribeUpdates(
        this.props.id,
        this.handleArticleUpdate
    )
}

可看出 class component 需要透過生命週期方法來處理副作用,開發者要在 componentDidMountcomponentDidUpdatecomponentWillUnmount 中分別考慮如何將資料同步到外部系統,並判斷各生命週期中應該做什麼來達到同步效果。當應用變複雜時就容易出錯。另外,component 內可能有多個副作用處理,不同副作用處理同時寫在 componentDidMountcomponentDidUpdatecomponentWillUnmount 中,容易產生衝突、難以維護與除錯。

Hooks

介紹完 class component 與其問題後,來看看 Hooks 吧! React Hooks 的出現就是為了解決 class component 的問題,React hooks 可以:

  • ✅ 為 function component 增加狀態
  • ✅ 管理元件的生命週期,而無需使用生命週期方法,例如: componentDidMountcomponentWillUnMount
  • ✅ 在多個元件間重用相同的狀態邏輯

接下來簡要介紹開發上常用的 useStateuseEffect hooks,因為之前在 Medium 文章已經詳細介紹過這兩個 hooks,詳情請見:[React] 認識狀態管理機制 state 與畫面更新機制 reconciliation[React] React 中的副作用處理、初探 useEffect,這裡就先簡單介紹,如果有想深入了解的可以再看看我之前的文章!

useState hook

useState hook 可管理 function component 內的狀態,useState 使用方式如下。

import { useState } from 'react';
export default function App(props){
    // 將 useState 回傳的值做陣列解構,第一個元素是「該次 render 的當前 state 值」,第二個元素是「用來更新 state 值的 setState 方法」,是一個函式
    // useState()傳入的參數 initialState 是 state 的初始值,可以是任意型別的值
    const [state, setState] = useState(initialState);
    //...
}

如果我們希望用 state 來控制使用者在 input 欄位輸入的值,可將 input 和 useState 搭配使用如下。

import { useState } from 'react';
function Input() {
  const [inputVal, setInputVal] = useState(""); // 使用輸入值,初始值為空字串
  
  // 當 input 的值改變時,呼叫 setInputVal 來改變 input 的 state 值
  // input 欄位的 value 就是我們儲存的 inputVal state 值
  return <input onChange={(e) => setInputVal(e.target.value)} value={inputVal} />;
}

useEffect hook

useEffect 可用來管理 component 內的副作用,並且讓開發者不須再使用生命週期方法來處理各生命週期階段要做的事,只要單純使用一個 useEffect 來處理即可。
useEffect 呼叫方式:

import { useEffect } from 'react';
export default function App(props){
    //...
    useEffect(effectFunction, dependencies?)
    //...
}

effectFunction 參數

  • 傳入一個函式,放副作用處理邏輯
  • 如果副作用造成的影響需要被清理,可讓 effectFunction 函式本身回傳一個清理副作用的 cleanup 函式
  • 此函式的執行時機:
    • component 每次 render 完成、且實際 DOM 被更新後,才執行
    • component unmount 時,會執行最後一次 render 的 cleanup 函式

dependencies 參數

  • 可選填的陣列參數
    • 陣列內應包含 effect 函式中所有依賴的 component 資料項目,如: props、state 或任何受資料流影響、可能會變動的延伸資料
  • dependencies 參數提供與否,會影響 effect 函式的執行
    • 若不提供 dependencies:effect 函式預設會在每次 render 後都執行一次
    • 若有提供 dependencies:React 會在 re-render 時用 Object.is 一一比較陣列中所有依賴項目的值與前一次 render 版本的是否相同,若相同,則跳過執行此次 render 的 effect 函式

使用範例

如果我們希望 input 的值改變時,能用 console.log 印出目前的值,就可使用 useEffect 來讓 console.log 的行為和 input 的值同步,換句話說,我們用 useEffect 來處理 console.log 的副作用行為。程式碼如下。

import { useState, useEffect } from 'react';
export default function Input() {
  const [inputVal, setInputVal] = useState("");

  useEffect(() => {
    console.log(`The user typed ${inputVal}`);
  }, [inputVal]);

  return (
    <input
      onChange={e => setInputVal(e.target.value)}
      value={inputVal}
    />
  );
}

Hooks 的規則

使用 React hooks 時有幾項規則需要遵守:

  • 只能在 component function 內被呼叫,hooks 需依賴 component 才能運作
  • 只能在 component function 的頂層作用域被呼叫,不能在條件式、迴圈或 callback 函式中呼叫

hooks 有這些限制是為了確保 hooks 機制正確運作,沒遵守這規定可能導致資料丟失問題,導致出現預期外的錯誤,詳細請見[React] 認識 useCallback、useMemo,了解 hooks 運作原理文章後半有說明。

優點

使用 Hooks 的優點如下:

  • 可在多個元件間重用有狀態的邏輯:可在不同元件間重用有狀態的邏輯(stateful logic),而不需重複撰寫程式碼
  • 可共享非視覺的邏輯:在 hooks 之前,React 無法提取和共享非視覺(nonvisual)邏輯,因而出現 HOC 模式和 Render Prop 來解決問題。而 hooks 出現後解決此問題,hooks 可將有狀態邏輯提取到 JavaScript 函式中,定義客製化 hooks(custom hooks)
  • 可和社群共享 custom hook,不須全部自己開發。開源 custom hook 如:
  • 可撰寫更少的程式碼:hooks 可讓開發者按照關注點和功能來區分程式碼,而非按照生命週期,程式碼因而更簡潔
  • 能簡化複雜的元件結構:hooks 以 JavaScript functional programming 的設計方式來簡化實作,不需撰寫複雜的 class component

缺點

使用 Hooks 的缺點如下:

  • 須遵守 hooks 的規則:建議用 linter 來提醒自己遵循 hooks 的規則
  • 需要長時間的練習才能知道如何正確使用(如:useEffect)
  • 需注意錯誤的用法(如:useCallbackuseMemo)

Reference


上一篇
[Day 20] Render Props 模式
下一篇
[Day 22] 程式碼拆分(Code Splitting)與動態匯入(Dynamic Import) (1)
系列文
30天的 JavaScript 設計模式之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言