紀錄工作經驗、相關知識,解決技術相關問題。

React, 新手 Jamp Holes

Class Component 是什麼?React 類別元件寫法、生命週期與函式元件的差異

Class Component 是什麼?React 類別元件寫法、生命週期與函式元件的差異 特色圖片

Class Component 是什麼? Class Component(類別元件)是用 JavaScript 的 class 語法定義的 React 元件:繼承 React 內建的 Component,並實作一個 render() 方法回傳 JSX。第一次遇到多半是在舊專案或舊教學裡,看到 this.state、this.setState、componentDidMount 這些名稱,卻發現新教學全都寫函式。本文說明類別元件的基本寫法、生命週期方法各在什麼時候執行、跟函式元件怎麼一一對照,以及目前哪些情況還是只能用類別。

JSX 本身的規則可前往〈JSX 是什麼?React JSX 語法規則、與 HTML 的差異和轉譯原理〉觀看,本文不重講,只談元件的寫法。

Class Component 是什麼:用 class 定義的元件

React 官方文件對 Component 的描述是:它是以 JavaScript 類別定義的 React 元件的基底類別,要定義類別元件,就繼承內建的 Component 並定義 render 方法。官方同時說明,類別元件仍然受 React 支援,但不建議用在新程式碼,官方建議把元件定義成函式。Component 在文件裡被歸在 Legacy React APIs 底下,這一組 API 仍從 react 套件匯出,但不建議用於新寫的程式碼。

最小的類別元件長這樣:

import { Component } from 'react';

class Greeting extends Component {
  render() {
    return <h1>Hello, {this.props.name}!</h1>;
  }
}

傳進來的 props 從 this.props 讀取,官方註明這等同於函式元件宣告 props。使用方式和函式元件相同,都是 <Greeting name="Taylor" />。

類別元件的 state:this.state 與 setState

類別元件的 state 放在 this.state,官方規定它必須是物件,也不可以直接修改,要改就呼叫 setState:

class Counter extends Component {
  state = { age: 42 };

  handleAgeChange = () => {
    this.setState({ age: this.state.age + 1 });
  };

  render() {
    return (
      <>
        <button onClick={this.handleAgeChange}>Increment age</button>
        <p>You are {this.state.age}.</p>
      </>
    );
  }
}

幾個容易搞混的地方:

  • setState 是「請求」:官方說明要把它想成請求更新元件,而不是立即執行的命令,呼叫之後 this.state 不會馬上變。
  • 只有 constructor 可以直接指定 this.state;其他方法一律用 setState,而且不要在 constructor 裡呼叫 setState。
  • 舊寫法會在 constructor 裡宣告 state,並用 this.handleClick = this.handleClick.bind(this) 綁定方法。官方說明 constructor 通常只用在這兩件事,而用現代語法(public class field)就能改寫成上面範例的 state = {...} 與箭頭函式,所以 constructor 很少需要了。
  • constructor 裡不該放副作用或訂閱。

官方也註明:在類別中定義 state,等同於在函式元件呼叫 useState。

生命週期方法:mount、update、unmount

React 元件從加入畫面到移除,會經過三個階段,類別元件用同名方法掛進這三個時間點:

方法執行時機(依官方說明)常見用途
componentDidMount元件加入畫面(mount)時開始資料請求、建立訂閱、操作 DOM 節點
componentDidUpdate因新的 props 或 state 重新渲染後立刻呼叫,初次渲染不會呼叫更新後操作 DOM;比較新舊 props 之後才發請求
componentWillUnmount元件從畫面移除(unmount)前取消資料請求、移除訂閱

官方特別提醒:componentWillUnmount 的邏輯應該要「對應」componentDidMount,在掛載時建立的東西,在卸載時要收掉。

下面這段程式碼把同一個計數器分別寫成類別元件與函式元件,並在每個生命週期印出 log。函式元件用 useEffect 加上清除函式,依賴陣列放 count:

class CounterClass extends Component {
  state = { count: 0 };

  componentDidMount() {
    log('[class] componentDidMount');
  }

  componentDidUpdate(prevProps, prevState) {
    log('[class] componentDidUpdate: ' + prevState.count + ' -> ' + this.state.count);
  }

  componentWillUnmount() {
    log('[class] componentWillUnmount');
  }

  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <button id="class-btn" onClick={this.handleClick}>
        Class: {this.state.count}
      </button>
    );
  }
}

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

  useEffect(() => {
    log('[function] effect run, count = ' + count);
    return () => {
      log('[function] cleanup, count = ' + count);
    };
  }, [count]);

  return (
    <button id="fn-btn" onClick={() => setCount(count + 1)}>
      Function: {count}
    </button>
  );
}

兩個按鈕在頁面上依序操作:點類別按鈕兩次、點函式按鈕一次,最後按「Unmount」把兩個元件移除。log 函式把訊息依序寫進頁面上的 <pre>:

Chrome 158、Playwright 1.64.1、React 19.3.1、Windows 11 的畫面:依序點擊兩個計數器按鈕再卸載後,log 依序顯示 componentDidMount、effect run、componentDidUpdate 兩次、cleanup 與 effect run、componentWillUnmount 與 cleanup(圖中為頁面上的 log 內容)

從圖中的順序可以對照:

  • 初次渲染:類別印 componentDidMount,函式印 effect run, count = 0。
  • 點類別按鈕:每次 setState 後印一行 componentDidUpdate,帶出前後的 count(0 -> 1、1 -> 2)。
  • 點函式按鈕:先跑上一輪的 cleanup, count = 0,再跑新的 effect run, count = 1。
  • 按下 Unmount:類別印 componentWillUnmount,函式印最後一次的 cleanup, count = 1。

這裡的重點是兩者「切分方式」不同:類別按時間點拆成三個方法,函式的 useEffect 則是把「設定」和「清除」寫在一起,並用依賴陣列決定何時重跑。官方的說法是:許多情況下,類別元件同時定義 componentDidMount、componentDidUpdate、componentWillUnmount,等同於在函式元件呼叫 useEffect;少數需要在瀏覽器繪製前執行的情況,useLayoutEffect 比較接近。官方也提醒:元件如果沒有和任何外部系統同步,可能根本不需要 Effect。

類別元件與函式元件對照

官方文件在 Component 頁面提供「從類別遷移到函式」的範例,對照起來是:

類別元件函式元件
this.props函式參數 props
this.state 與 this.setStateuseState
componentDidMount、componentDidUpdate、componentWillUnmountuseEffect(需要在繪製前執行時用 useLayoutEffect)
static contextType 與 this.contextuseContext

同一個元件用兩種寫法渲染,輸出的 HTML 一樣。下面用 react-dom/server 的 renderToStaticMarkup 輸出:

class GreetingClass extends Component {
  state = { count: 0 };
  render() {
    return <h1>Hello, {this.props.name}! ({this.state.count})</h1>;
  }
}

function GreetingFunction({ name }) {
  const [count] = useState(0);
  return <h1>Hello, {name}! ({count})</h1>;
}

const a = renderToStaticMarkup(<GreetingClass name="Taylor" />);
const b = renderToStaticMarkup(<GreetingFunction name="Taylor" />);
console.log('class   :', a);
console.log('function:', b);
console.log('same HTML:', a === b);
Node.js v22.20.0、React 19.3.1、Windows 11 輸出的結果:類別元件與函式元件都輸出 h1 的 Hello, Taylor! (0),same HTML 為 true(圖中為腳本輸出內容)

差別不在輸出,而在寫法與可用的功能:函式元件不用處理 this、不用綁定方法,也能用 Hooks 把相關邏輯放在一起。官方文件的遷移範例也以「Typically, you will define components as functions instead」開頭,意思是一般會改用函式定義元件。

Error Boundary:目前仍需要類別元件的情況

官方文件指出,React 預設在渲染過程丟出錯誤時,會把整個 UI 從畫面移除。要避免這種情況,可以用 Error Boundary 包住一部分 UI:它是特殊的元件,子元件出錯時改顯示備用畫面(fallback UI)。

要實作 Error Boundary,官方要求提供 static getDerivedStateFromError,用來在出錯後更新 state,也可以選擇性實作 componentDidCatch 記錄錯誤。官方文件在這兩個方法下都註明:函式元件目前沒有直接對應,並且明講「目前沒有辦法用函式元件寫 Error Boundary」。所以這是類別元件目前還有不可取代用途的地方。

class ErrorBoundary extends Component {
  state = { hasError: false };

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    const el = document.getElementById('log');
    el.textContent = 'componentDidCatch: ' + error.message;
  }

  render() {
    if (this.state.hasError) {
      return <p id="fallback">Something went wrong.</p>;
    }
    return this.props.children;
  }
}

function Broken() {
  throw new Error('render failed');
}

把正常的子元件換成 Broken 之後,畫面變成備用內容,componentDidCatch 收到錯誤訊息:

Chrome 158、Playwright 1.64.1、React 19.3.1、Windows 11 的畫面:按下按鈕改渲染會丟出錯誤的子元件後,畫面顯示 Something went wrong.,下方 log 顯示 componentDidCatch: render failed(圖中為頁面內容)

官方也列出 Error Boundary 不會捕捉的錯誤:事件處理器內的錯誤、伺服器端渲染(Server side rendering)、Error Boundary 自身丟出的錯誤,以及非同步程式碼(例如 setTimeout 或 requestAnimationFrame 的回呼)中的錯誤;startTransition 是例外,其中的錯誤會被捕捉。

舊的生命週期方法:componentWillMount 等已棄用

舊教學常見 componentWillMount、componentWillReceiveProps、componentWillUpdate。官方說明這三個 API 已改名為加上 UNSAFE_ 前綴的版本,舊名稱已棄用,未來的主要版本將只有新名稱可用;官方並提供 rename-unsafe-lifecycles codemod 協助自動更新。官方對 UNSAFE_componentWillMount 的說法是:它只因歷史原因而存在,不該用在任何新程式碼。看到舊專案有這些方法時,別直接模仿,改用上面表格的方法或函式寫法。

shouldComponentUpdate 與 PureComponent

類別元件可以定義 shouldComponentUpdate 決定是否略過重新渲染。官方說明這個方法只是效能最佳化,如果沒有它元件就會壞掉,應該先修那個問題;也建議考慮用 PureComponent,而不是手寫 shouldComponentUpdate。

static defaultProps:類別元件仍保留

類別元件可以用 static defaultProps 設定預設 props,官方說明它用於 undefined 與缺少的 props,不用於 null。React 19 升級指南提到:React 19 移除了函式元件的 defaultProps,改用 ES6 預設參數;類別元件因為沒有 ES6 替代方案,會繼續支援。TypeScript 專案與函式元件 defaultProps 的處理方式,可前往〈解決 React 元件 defaultProps 錯誤:TypeScript Props 預設參數、HOC 與重構踩坑排查〉觀看。

學 Class Component 還有用嗎?

整理官方文件的立場:

  • 新元件:官方建議用函式,類別元件不建議用在新程式碼。
  • 舊專案:類別元件仍受支援,不是非改不可;讀得懂 this.state、生命週期與 setState,就能維護既有程式碼。
  • 要寫 Error Boundary:目前仍要用類別。
  • 要遷移:官方的做法是逐項對照,先確認 componentWillUnmount 做的事和 componentDidMount 相反,再把這組邏輯改寫成一個 Effect。

要自己建立專案練習,可前往〈React 工具鏈規格解析:基於 Vite 的初始化流程、目錄架構與生產環境建構〉觀看。

Class Component 重點整理

項目說明
定義繼承 Component、實作 render() 的 JavaScript 類別
props 與 statethis.props、this.state;改 state 用 setState,不可直接修改
生命週期componentDidMount、componentDidUpdate、componentWillUnmount 三個時間點
對應函式寫法useState、useEffect、useContext
已棄用componentWillMount、componentWillReceiveProps、componentWillUpdate 舊名稱
仍需類別Error Boundary
官方立場仍支援,但新程式碼建議用函式元件

參考資料與延伸閱讀

發佈留言