React 元件生命週期 (Component Lifecycle)

一般常會用 JavaScript Class 語法來宣告 React Component:

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

在這一篇文章中,我們會來更深入介紹 React Component Class 有哪些屬性 (Properties) 和方法 (Methods)。

React Component Class 除了 render(),還有一系列的 Lifecycle Methods (方法),讓你可以在不同的元件事件 (也就是所謂的元件生命週期) 發生時做點你想做的事。

Lifecycle Methods 如果是 will 開頭的,表示在某個事件發生「之前」,會執行這個方法;如果是 did 開頭的,表示在某個事件發生「之後」,會執行這個方法。

Lifecycle Methods 可以分為三大類:

  1. Mounting - 裝載,當元件被加入到 DOM 中時會觸發
  2. Updating - 更新,當元件的 props 或 state 更新,重新渲染 (re-rendered) DOM 時會觸發
  3. Unmounting - 卸載,當元件要從 DOM 中被移除時會觸發
  4. Error Handling - 例外處理,當元件發生 JavaScript errors 時會觸發

Mounting Lifecycle Methods

React 提供有這些 Mounting 階段的方法:

render()

render() 是 React Component 唯一一定要實作的方法。

render() 在每次 props 或是 state 被改變時,都會被執行一次。

在 render() 中你會根據當前 this.propsthis.state 的資料狀態,來決定該元件當前的 UI 結構和顯示內容。

render() 可以返回下面這幾種資料型態其一:

  • React elements

    通常 render() 返回的就是用 JSX 建立的 React 元素。

  • String / numbers

    你也可以返回字串或是數字,這會被當作是 HTML DOM 的 text nodes 來顯示。

  • null

    返回 null 告訴 React 不顯示任何東西。

  • Booleans

    返回布林值 false 也是告訴 React 不顯示任何東西。Booleans 通常是為了這種寫法 pattern:

    render() {
      // .....
      return test && <Child />;
    }
    

    其中 test 是一個布林值,當作一個開關值 (flag) 決定是否要顯示 <Child />

實作慣例上,我們會保持 render() 是一個 pure function,不會在裡面更改 State 值,也不會在裡面寫任何會造成 side-effects 的 code (例如和瀏覽器互動)。

constructor(props)

元件 Class 的 constructor (建構子) 會在元件還沒被掛載到 DOM 之前先被執行來做初始化。

React 元件是 React.Componentsubclass,你必須在 constructor 的最前面 call super(props) 否則會發生問題,像是會沒有 this.props

constructor 是用來初始化 (initialize) state 的地方,直接將初始值指定 (assign) 給 this.state 這屬性,你不能在 constructor 中使用 setState() 喔。

用法例子:

constructor(props) {
  super(props);
  this.state = {
    color: props.initialColor
  };
}

componentWillMount()

componentWillMount() 只會被執行一次,會在元件被掛載到實際的 HTML DOM 之前被呼叫執行。

componentWillMount() 會在第一次的 render() 執行之前就先被執行,所以你不能在 componentWillMount() 中做跟 DOM 有關的操作。

通常 componentWillMount() 比較少會用到,實用性不大,大部分可以在 componentWillMount() 做的事都可以也更適合在 constructor() 中完成。

componentDidMount()

componentDidMount() 會在元件被掛載到 DOM 後被執行 - 也就是說元件已經實際存在在畫面中,任何需要 DOM 或會 Asynchronous 更新 state 狀態的操作都適合放在 componentDidMount() 做。

適合在 componentDidMount() 做的,像是綁定元件的 DOM 事件,或 AJAX 拉遠端資料來進一步初始化元件。

Updating Lifecycle Methods

React 提供有這些 Updating 階段的方法:

componentWillReceiveProps(nextProps)

componentWillReceiveProps() 會在每次元件接收到 props 更新時被執行,通常我們會在 componentWillReceiveProps() 中聽 props 的改變來更新元件對應的 State 值。

componentWillReceiveProps() 會傳進 nextProps 這參數,表示上一次更新時的舊 props 值,讓你可以運用來比對 props 前後值的變化,因為有時候不一定是 props 有更新才會 call componentWillReceiveProps(),當父元件刷新子元件時也會執行 componentWillReceiveProps()。

元件第一次 render() 時,React 不會 call componentWillReceiveProps()。

使用例子:

componentWillReceiveProps(nextProps) {
  if (this.props.initialX !== nextProps.initialX) {
    this.setState = ({
      // .....
    });
  }
}

shouldComponentUpdate(nextProps, nextState)

shouldComponentUpdate() 是用來你想最佳化效能 (performance) 時使用,每當 Props 或 State 有更新時,React 會在 call render() 重繪畫面之前,先 call shouldComponentUpdate() 決定是否真的需要 render()。

React 雖然會很聰明地自動偵測到 Props 和 State 狀態有改變來自動更新元件 (re-render),但有些狀況下你可以幫助 React 更精確判斷元件是否真的需要更新,這就是 shouldComponentUpdate() 的功用了。

你可以利用 shouldComponentUpdate() 傳進的參數 nextProps, nextState (新的 props 和 state 值) 和當前的 this.props, this.state 來判斷元件資料狀態變化,shouldComponentUpdate() 執行後得返回一個布林值 (Boolean) 來告訴 React 是否需要更新元件,如果返回 falsecomponentWillUpdate(), render(), componentDidUpdate() 這些 Lifecycole Methods 就不會被執行。

若你沒實作 shouldComponentUpdate() 預設上返回 true

componentWillUpdate(nextProps, nextState)

componentWillUpdate() 會在元件準備更新、執行 render() 之前被執行。

在 componentWillUpdate() 中,禁止有任何會更新到元件的動作,像是不能 call this.setState(),如果你會更新 State 請用 componentWillReceiveProps()

在大部分的情形下,其實 componentWillUpdate() 實用性並不高。

元件第一次 render() 時,React 不會 call componentWillUpdate()。

componentDidUpdate(prevProps, prevState)

componentDidUpdate() 會在元件更新完成、執行完 render() 重繪後被執行。

在這邊可以執行像在 componentDidMount() 中的操作,比對 prevProps/prevState 及 this.props/this.state 狀態差異,做像是存取 DOM、重畫 Canvas、重整頁面 layout、AJAX 網路呼叫等動作。

元件第一次 render() 時,React 不會 call componentDidUpdate()。

Unmounting Lifecycle Methods

React 提供有這些 Unmounting 階段的方法:

componentWillUnmount()

當元件將要從 DOM 中被移除之前,React 會執行 componentWillUnmount()。

你可以在 componentWillUnmount() 中做資源清理的動作,清除跟這元件有關的任何遺留物,像是清除你在 componentDidMount() 中建立的資源,例如清除 timer、取消 AJAX request、移除 event listener 等。

Error Handling Lifecycle Methods

React 提供有這些例外處理的方法:

componentDidCatch(error, info)

React 在 V16 版後新增了錯誤邊界 (Error Boundary) 的概念,新加入了 componentDidCatch() 可以用來捕捉 (catch) 從子元件 (child component tree) 中拋出的錯誤 (JavaScript errors),避免因為一個小元件發生意外錯誤就造成整個頁面掛掉,讓錯誤不會影響到邊界外層的父元件,你可以在 componentDidCatch() 決定這個例外錯誤該怎麼處理,像是 fallback UI。

想要成為一個 Error Boundary 的方式很簡單,只要在 Component 中加入 componentDidCatch() 方法就成為 Error Boundary 元件,每當子元件發生錯誤時,會被 Error Boundary 捕捉,不會讓錯誤延伸影響到錯誤邊界外層。

Error Boundary 的限制:

  • 只能捕捉子元件的錯誤,不包含 Error Boundary 元件本身
  • 只能捕捉從 constructor(), render() 和各 Lifecycle Methods 中發生的錯誤
  • 非同步 (Asynchronous) 程式中發生的錯誤無法被捕捉
  • Event Handler 發生的錯誤無法被捕捉

Error Boundary 實作範例:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  componentDidCatch(error, info) {
    // 顯示 fallback UI
    this.setState({ hasError: true });
    // 可以將 error 錯誤訊息記錄下來
    logErrorToMyService(error, info);
  }

  render() {
    if (this.state.hasError) {
      // 顯示 fallback UI
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}

然後這樣使用 Error Boundary:

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

元件生命週期事件順序

整理一下整個 Component 的生命週期發生的事件順序。

當元件第一次 render 時的順序:

  1. constructor()
  2. componentWillMount()
  3. render()
  4. componentDidMount()

此後,當元件被更新 (update) 時的順序:

  1. componentWillReceiveProps()
  2. shouldComponentUpdate() - 如果 return false 就不會再往下走
  3. componentWillUpdate()
  4. render()
  5. componentDidUpdate()

其中不能執行 this.setState() 的事件:

  • render()
  • componentWillMount()
  • shouldComponentUpdate()
  • componentWillUpdate()

元件的方法和屬性

之前我們介紹過的元件方法 (Method) 有 setState(),而屬性 (Properties) 有 propsstate。再來介紹幾個元件的方法和屬性:

component.forceUpdate(callback)

通常元件會根據 Props 和 State 的改變來更新元件,但有時候元件會依賴其他的資料,這時你就需要用 this.forceUpdate() 來告訴 React 你要更新元件。

執行 forceUpdate() 後 React 會跳過 shouldComponentUpdate() 直接執行 render() (但子元件的 Lifecycle Methods 都會被正常執行)。

而參數 callback 是一個 function,當元件更新完後會被執行。

一般實作上,需要盡量避免使用到 forceUpdate(),在 render() 中只用 this.props 和 this.state。

defaultProps

defaultProps 是一個 class property,用來設定當 props 是 undefined 時的預設值。

使用例子:

class CustomButton extends React.Component {
  // ...
}

CustomButton.defaultProps = {
  color: 'blue'
};

當沒有設定 props.color 時,props.color 值會是 blue:

render() {
  return <CustomButton /> ;
}

只有當 props 是 undefined 才會用 defaultProps 的值,如果值是 null 時則還是 null

render() {
  // props.color 的值會不動還是 null
  return <CustomButton color={null} />;
}

Class Fields + Arrow Function

React 內建的 Lifecycle 方法裡的 this 都會正確指向 Component 本身,但如果是你自己新增的方法,你需要在 constructor() 自己綁定 (bind) this。

例如:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    // 要自己 bind this
    this.handleClick = this.handleClick.bind(this);
  }
  
  render() {
    return (
      <button onClick={this.handleClick}>
        This is a button
      </button>
    )
  }
  
  handleClick() {
    this.updateList();
  }
}

但更方便的方式是用 Class Fields (類別屬性) 語法和 ES6 的 Arrow function 讓 this 被自動綁定:

class MyComponent extends React.Component {
  
  // ...
  
  handleClick = () => {
    this.updateList();
  }
}

Class Fields 當前還在 ECMAScript proposal stage 3 的階段,但你還是可以使用,因為反正寫 React 本來就會用 Babel 來改寫 JSX 了,Babel 也有 Class Fields Plugin