{ PH_Dev }

Published on

Class-based Component 生命週期方法(續)

Authors
  • avatar
    Name
    Penghua Chen(PH)
    Twitter

Class-based Component 生命週期方法(續)

今天的篇幅要延續昨天的部分繼續,要來看看在官方文件中如何定義了這些方法,以及哪些是常用的、應用在什麼地方。

學習順序會依照元件建立、更新及銷毀的生命週期順序來學習。

並在最後透過發一個非同步請求的範例來作為 ending,事不宜遲,趕緊來學習吧!

元件(Component)建立時(mounting)的生命週期方法

所有方法:

  • constructor
  • static getDerivedStateFromProps
  • render
  • componentDidMount

常用方法:

  • constructor
  • render
  • componentDidMount

contructor

首先在 mounting 階段時,最先被初始化的是 constructor 的部分,而這部分同時也是 JS class 類別,文件中提到:

通常在 React 中 constructor 只會有兩種用途:

  • 透過指定一個 this.state 物件來初始化內部 state。
  • 為 event handler 方法綁定 instance。

於是我們常常會有如下的設計:

import React, { Component } from 'react';

class Test extends Component{
  constructor() {
    super();
    this.state = {
      persons: [
        { name: 'Bill' }
      ]
    };
    
    this.changeNameHandler = () => {
      // do something...
    }
  }
}

而由於 ES7 中將 Class 的寫法簡化,所以我們可以將上方改寫成如下的方式:

import React, { Component } from 'react';

class Test extends Component{
  state = {
    persons: [
      { name: 'Bill' }
    ]
  }
  changeNameHandler = () => {
    // do something...
  }
}

此外文件中還有提到:

Constructor 應該要是開發者唯一指定 state 的地方,其餘地方則是透過 this.setState 的方式修改 state 的狀態。

==直接修改 state 的資料是雖然會改變 state 的狀態,但是並不會觸發畫面的更新。==

static getDerivedStateFromProps(props, state)

當子元件中具有自己的 state,並且這個 state 的改變是依賴父層 props 進來的值,那就可能需要用到這個方法。

此時會需要回傳一個物件來更新 state,但是如果並不需要更新的話,那其實也可以直接回傳 null 即可。

這邊我們設計一種情境:

父元件、子元件中皆有一個 radio box,切換任意一個的狀態時,都需要可以同步更新另一個 radio box 的狀態。

相關測試程式碼,點擊前往

// App.js
import React, { Component } from 'react';
import './App.css';
import SubComponent from './SubComponent';

class App extends Component {
  constructor() {
    super();
    this.state = {
      isTrue: true
    };
  }

  changeRadioHandler = e => {
    const value = e.target.value;
    const checkTrue = value === 'true'
      ? true
      : false;
    this.setState(state => {
      return {
        isTrue: checkTrue
      }
    })
  }

  render() {
    return (
      <div className="App">
        { this.state.persons.map((person, idx) => (
          <Card 
            key={ idx }
            changeRadioHandler={ this.changeRadioHandler }
            isTrue={ this.state.isTrue }
          /> 
        ))}
        <br />
        True: <input onChange={ e => this.changeRadioHandler(e) } checked={ this.state.isTrue } type="radio" name="main" value="true"/>
        False: <input onChange={ e => this.changeRadioHandler(e) } checked={ !this.state.isTrue } type="radio" name="main" value="false"/>
      </div>
    );
  }
}

export default App;
// SubComponent.js
import React, { Component } from 'react';
import './index.css';

class SubComponent extends Component {
  constructor() {
    super();
    this.state = {
      isTrue: true
    }
  }

  static getDerivedStateFromProps(props, state) {
    console.log(props);
    console.log(state);

    if(props.isTrue != state.isTrue) {
      return {
        isTrue: props.isTrue
      };
    } else {
      return null;
    }

  }

  render() {
    return (
      <div className="sub">
        <h1>Sub Component</h1>
        <br />
        True: <input onChange={ e => this.props.changeRadioHandler(e) } checked={ this.state.isTrue } type="radio" name="sub" value="true"/>
        False: <input onChange={ e => this.props.changeRadioHandler(e) } checked={ !this.state.isTrue } type="radio" name="sub" value="false"/>
      </div>
    );
  }
}

export default SubComponent;

SubComponent 元件中使用了 getDerivedStateFromProps 方法,可以透過 props 取得外層傳入的最新值,透過比對元件自身的 state 決定是否要更新子元件中的 state 狀態,而這個就是一個簡單的使用情境。

render

  1. render()class component 中唯一必要的方法。
  2. 透過 render 方法可以在瀏覽器上渲染出我們需要的 DOM。
  3. 此外,不應該在 render 方法中改變 component 的 state

而 render 方法中接受以下的值(關於 portal 與 fragement 的細節留待後續學習)

  • React element
// ... 略
render() {
  <div>
    <MyComponent />
  </div>
}
// ... 略
  • Array 和 fragment

透過 array 產生數個元件

// ... 略
render() {
  <div>
   { 
    [1,2,3].map(num => (
      <MyComponent />
    )) 
   }
  </div>
}
// ... 略

透過 Fragment 減少額外新增一個節點

// ... 略
render() {
  <React.Fragment>
     <ChildA />
     <ChildB />
     <ChildC />
  </React.Fragment>
}
// ... 略

等同於:

// ... 略
render() {
  <>
     <ChildA />
     <ChildB />
     <ChildC />
  </>
}
// ... 略
  • Portal

它們讓你將 children render 到不同的 DOM subtree 中

// 參考官方範例
return ReactDOM.createPortal(
  this.props.children,
  domNode
);
  • String 和 number
// ... 略
render() {
  <div>
    <h1>Hello world!</h1>
  </div>
}
// ... 略
  • Boolean 或 null

test 作為 flag,當 test 為 true 時才渲染 <Child />

// ... 略
render() {
  <div>
    test && <Child />
  </div>
}
// ... 略

componentDidMount

這個方法是一個蠻重要的方法,依據官方描述,==我們可以在這個方法中做網路請求==

意思是如果需要透過 call api 取得一些資料的話,在這個方法中非常適合。

而文件中有描述到:

這個方法適合設立任何 subscription。設立完 subscription 後,別忘了在 componentWillUnmount() 內取消 subscription。

這是什麼意思呢? 意思是我們可能有時候會需要透過額外建立像是監聽事件:

const div = document.querySelector('div');
div.addEventListener('click', fn);

我們可以在 componentDidMount 中建立類似這種監聽事件,並於元件銷毀時取消:

// 在 componentWillUnmount life cycle method 中取消
div.removeEventListener('click', fn);

另外就是可以在componentDidMount() 內呼叫 setState(),而這會觸發額外的一次 render ,但是會在瀏覽器更新螢幕之前發生。代表使用者並不會察覺到變化。

但使用上需要注意,可能會導致效能問題

相關測試範例,點擊前往

元件(Component)更新時(updating)的生命週期方法

所有方法:

  • static getDerivedStateFromProps()
  • shouldComponentUpdate()
  • render()
  • getSnapshotBeforeUpdate()
  • componentDidUpdate()

常用方法:

  • render()
  • componentDidUpdate()

shouldComponentUpdate(nextProps, nextState)

透過 shouldComponentUpdate 可以用來決定 component 會不會跟著被更新的 state 或者 props 而有所變動。

當我們回傳 false 時,可以阻止 component 重新 render。

但 React 的預設行為是每次只要有更新就會觸發重新 render,所以在使用這個方法時要特別注意。雖然這個方法的存在是為了效能最佳化,使我們可以透過比對前後的 state, prop 來決定是否做此次的更新,但透過回傳 false 並不會避免子元件在自身 state 改變時的重新 render

// ...
shouldComponentUpdate(nextProps, nextState) {
 // 可以在這裡比對 state, props 決定是否要做此次更新
  return true;
}
// ...

getSnapshotBeforeUpdate(prevProps, prevState)

在最近一次 render 前可以讓我們做些處理的方法,通常可以用來在 DOM 改變之前先從其中抓取一些資訊,此外當使用了這個方法時,會回傳一個值作為參數傳遞給 componentDidUpdate()

最常使用的情況是用來處理 取得滾動軸位置,以官方的例子為例:

class ScrollingList extends React.Component {
  constructor(props) {
    super(props);
    this.listRef = React.createRef();
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // Are we adding new items to the list?
    // Capture the scroll position so we can adjust scroll later.
    if (prevProps.list.length < this.props.list.length) {
      const list = this.listRef.current;
      return list.scrollHeight - list.scrollTop;
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // If we have a snapshot value, we've just added new items.
    // Adjust scroll so these new items don't push the old ones out of view.
    // (snapshot here is the value returned from getSnapshotBeforeUpdate)
    if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      <div ref={this.listRef}>{/* ...contents... */}</div>
    );
  }
}

透過在 getSnapshotBeforeUpdate 做些判斷並回傳值,作為 componentDidUpdate 中可以用來判斷並額外處理的值。

componentDidUpdate(prevProps, prevState, snapshot)

這是更新時最重要的方法,在更新後會馬上被呼叫。

而依據官方解釋,在這邊也非常適合做網路請求,這代表我們也可以在這個生命週期 call api。

componentDidUpdatecomponentDidMount 用法類似,也可以在此呼叫 setState 來更新 state 的值,但需要特別注意的是,componentDidUpdate 中記得比較前後的值是否有差異,如果有差異的話才做新的一次網路請求,否則可能會影響 component 的效能。

此官方例子為例:

componentDidUpdate(prevProps) {
  // 常見用法(別忘了比較 prop):
  if (this.props.userID !== prevProps.userID) {
    this.fetchData(this.props.userID);
  }
}

元件(Component)卸下時(Unmount)的生命週期方法

最後是元件生命週期的最後一哩路,就是銷毀或卸下這個元件。

這個部分的生命週期方法只有 componentWillUnmount會在一個 component 被 unmount 和 destroy 後馬上被呼叫,我們可以在這裡做一些清除的行為。

鐵人賽文章與程式碼同步發佈於:

資源