[React] 理解 getDerivedStateFromProps 生命週期於父子組件溝通上的應用

以實例理解來父子組件互相溝通方式及 getDerivedStateFromProps 生命週期所扮演的角色。

前言


學習任何一個前端框架,筆者認為最基礎的就是要了解組件跟組件的互動方式,畢竟網站就是靠這些組件堆積而成,如果無法瞭解基本溝通方式,那要怎樣建立出一個符合期待的可復用組件供大家使用呢?因此本文以一個簡單的父子組件同步資料功能為例,理解如何以單向資料流概念完成此需求。

測試版本:react v16.11.0

 

 

需求說明


1. 父組件(灰色區塊):輸入框修改數值時(phone state),子組件輸入框也要同步顯示修改後的數值。
2. 子組件(黃色區塊):輸入框修改數值時(phone state),父組件輸入框也要同步顯示修改後的數值。

為檢視方便,會在父組件印出組件內 phone state 值於輸入框下方,另外子組件輸入框下方除組件內 phone state 印出外,也一併把接收父組件數值的 value prop 印出顯示。

 

 

概念探討


思考一下如果要完成此需求,實作的資料流方向大致如下:

  1. 在父組件定義 phone state 並顯示於 input 中。
  2. 當 input 被修改時,同步異動 phone state 值。
  3. 父組件的 phone state 經由 value prop 傳入子組件。
  4. 當 value prop 變動時同步異動子組件的 phone state 值。
  5. 子組件定義的 phone state 顯示於 input 中。
  6. 當 input 被修改時,透過 onChange prop 通知父組件。
  7. 父組件收到異動通知時,同步異動 phone state 值。

    (接續執行步驟1及3)

 

 

父組件


先從父組件著手,定義 phone state 後綁上 input 框,當 input 變動時同步靠 handlePhoneChange 函式修改 phone state 值,並訂定 handleChildPhoneChange 函式接受子組件數值變化事件,當子組件數值變化時同步修改復組件中的 phone state 值,達到數值同步目的。

import React from 'react'
import Child from './Child'

class Parent extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      phone: '0922123123'
    }
  }

  handlePhoneChange = e => {
    // 當 input 被修改時,同步異動 phone state 值
    this.setState({ ...this.state, phone: e.target.value })
  }

  handleChildPhoneChange = newPhone => {
    // 收到異動通知時,同步異動 phone state 值
    this.setState({ ...this.state, phone: newPhone })
  }

  render () {
    const { phone } = this.state
    return (
      <>

        {/* 父組件 */}
        <div>請輸入手機號碼</div>
        <input type='text' value={phone} onChange={this.handlePhoneChange} />
        <div> 內部使用的數值(phone state): {phone} </div>

        {/* 子組件 */}
        <Child value={phone} onChange={this.handleChildPhoneChange} />

      </>
    )
  }
}

export default Parent

 

 

子組件 - 使用 getDerivedStateFromProps 抄寫 prop 至 state


在子組件中的首要之務就是當 value prop 變動時可以將值抄寫到 phone state 中 (因無法直接針對 prop 值做異動),所以 static getDerivedStateFromProps(nextProps, prevState) 生命週期就是最佳選擇,因這個生命週期就是為了取代官方已不建議再使用的 componentWillReceiveProps 生命週期而存在,我們可將新傳入的 props 抄寫到 state 上面回傳,另外當新傳入的 props 無須異動 state 時可回傳 null 表示之。

getDerivedStateFromProps 是一個 static 靜態函數,因此無法操作 class 中的屬性 (ex. this.state)。
getDerivedStateFromProps 在每次 re-rendering 前會被調用,表示當 state 變化時也會重新被調用。

 

務必記得在 componentWillReceiveProps 操作 prop 抄寫到 state 這類行為時一定要加上條件,否則父層只要 rerender 就會觸發此生命週期 (無論 prop 是否有變化),就會不小心洗掉組件內變更後的 state 值了,所以在此例中我們僅當傳入的 value prop 跟組件內 phone state 值不同時才執行抄寫;緊接著再來處理 phone state 值,將 phone state 綁上 input 框,當 input 變動時同步靠 handlePhoneChange 函式觸發 onChange prop 函式,通知父組件 phone state 值已經被動了。

import React from 'react'
import PropTypes from 'prop-types'

class Child extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      phone: ''
    }
  }

  static getDerivedStateFromProps (props, state) {
    // 將 value prop 同步異動子組件的 phone state 值
    // (判斷 value prop 與 phone state 不同時才變動)
    if (props.value !== state.phone) {
      return { ...state, phone: props.value }
    }

    return null // 回傳 null 表示 state 無異動
  }

  handlePhoneChange = e => {
    // 當 input 被修改時,透過 onChange prop 通知父組件
    this.props.onChange(e.target.value)
  }

  render () {
    const { value } = this.props
    const { phone } = this.state

    return (
      <>
        <div>變動資料可通知父層組件來同步資料</div>
        <div><input type='text' value={phone} onChange={this.handlePhoneChange} /></div>
        <div>
          外部傳入的數值(value prop): {value} <br />
          內部使用的數值(phone state): {phone}
        </div>
      </>
    )
  }
}

Child.propTypes = {
  value: PropTypes.string,
  onChange: PropTypes.func
}

export default Child

 

看一下效果,完美達成需求。

 

 

等等.. 好像只能當乖兒子組件!?


我們可以發現此子組件過度依賴父組件,因為若父組件不在 onChange 事件中修改 phone state 值時,子組件就無法接收新的 value prop 數值去變動本身的 phone state 值,因此完全無法獨立運作阿!

在組件的設計上會希望能夠兼容各種情境,若希望在子組件中不要仰賴父組件的行為就能運作,我們必須在子組件的 input 變動時也同步調整內部 phone state 數值 (如下圖紅框部分)。

 

代碼依情境調整一下,故意讓父組件忽略子組件的數值異動事件 handleChildPhoneChange 處理行為。

class Parent extends React.Component {
  
  // ... 略 ...

  handleChildPhoneChange = newPhone => {
    // [移除]
    // 收到異動通知時,同步異動 phone state 值
    // this.setState({ ...this.state, phone: newPhone })
  }

  render () {
    const { phone } = this.state
    return (
      <>

        {/* 父組件 */}
        <div>請輸入手機號碼</div>
        <input type='text' value={phone} onChange={this.handlePhoneChange} />
        <div className='tp-hint'> 內部使用的數值(phone state): {phone} </div>

        {/* 子組件 */}
        <Child value={phone} onChange={this.handleChildPhoneChange} />

      </>
    )
  }
}

export default Parent

 

子組件當 input 被修改時,除了通知父組件外,亦補上同步異動本身 phone state 值的代碼。

class Child extends React.Component {
  
  // ... 略 ...

  handlePhoneChange = e => {
    // 當 input 被修改時,透過 onChange prop 通知父組件
    this.props.onChange(e.target.value)

    // [補上]
    // 當 input 被修改時,同步異動 phone state 值
    this.setState({ ...this.state, phone: e.target.value })
  }

  render () {
    const { value } = this.props
    const { phone } = this.state

    return (
      <>
        <div>變動資料可通知父層組件來同步資料</div>
        <div><input type='text' value={phone} onChange={this.handlePhoneChange} /></div>
        <div>
          外部傳入的數值(value prop): {value} <br />
          內部使用的數值(phone state): {phone}
        </div>
      </>
    )
  }
}


 

驗證一下結果囉~~

咦~~ 跟我想的不一樣,怎麼還是無法修改子組件的 phone state 呢?

 

 

修正 getDerivedStateFromProps 禁區


重新審視一下程式發現我們忽略 getDerivedStateFromProps 重要的特性。

getDerivedStateFromProps 在每次 re-rendering 前會被調用,表示當 state 變化時也會重新被調用。

 

原來是當子組件輸入框變動讓 phone state 變化後,觸發 re-rendering 前會重新調用 getDerivedStateFromProps 方法,此時判斷傳入的 value prop 跟目前變動後的 phone state 值不相同,所以又將 phone state 修改回父組件的值,因此我們才會看起來沒變化。

class Child extends React.Component {

  // ... 略 ...

  static getDerivedStateFromProps (props, state) {
    // 將 value prop 同步異動子組件的 phone state 值
    // (判斷 value prop 與 phone state 不同時才變動)
    if (props.value !== state.phone) {
      return { ...state, phone: props.value }
    }

    return null // 回傳 null 表示 state 無異動
  }

  // ... 略 ...

}


 

稍微調整一下代碼,透過 prevPropValue state 紀錄前一次 value prop 數值,比較傳入值確實有變化時才更新 phone state 即可。

import React from 'react'
import PropTypes from 'prop-types'

class Child extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      phone: '',
      prevPropValue: '' // 存放前一次的 value prop
    }
  }

  static getDerivedStateFromProps (props, state) {
    // 將 value prop 同步異動子組件的 phone state 值
    // (判斷 value prop 與 前一次的 value prop 不同時才變動)
    if (props.value !== state.prevPropValue) {
      return { ...state, phone: props.value, prevPropValue: props.value }
    }

    return null // 回傳 null 表示 state 無異動
  }

  handlePhoneChange = e => {
    // 當 input 被修改時,透過 onChange prop 通知父組件
    this.props.onChange(e.target.value)

    // 當 input 被修改時,同步異動 phone state 值
    this.setState({ ...this.state, phone: e.target.value })
  }


  render () {
    const { value } = this.props
    const { phone } = this.state

    return (
      <>
        <div>變動資料可通知父層組件來同步資料</div>
        <div><input type='text' value={phone} onChange={this.handlePhoneChange} /></div>
        <div>
          外部傳入的數值(value prop): {value} <br />
          內部使用的數值(phone state): {phone}
        </div>
      </>
    )
  }
}

Child.propTypes = {
  value: PropTypes.string,
  onChange: PropTypes.func
}

export default Child

 

最後驗證一下結果,即使父層不處理子組件的 onChange 事件,子組件仍可運作得好好的,所以這種組件特性的應用情境為「可重複性初始化」組件,也就是在任何時間點當初始參數 value prop 值變動時,可直接讓子組件套用新初始值,此外子組件又可依據初始值去做後續數值邏輯上的變動,結果如下。

 

 

還有其他方式嗎?


除了 getDerivedStateFromProps 可以將 prop 抄寫到 state 外,思考一下是否有其他可行的方案?有的,我們亦可使用 componentDidMount 搭配 componentDidUpdate 方式實作,這樣也是可以達成我們的需求,但代碼分散且複雜了,因此處理這類需求的首選還是 getDerivedStateFromProps 生命週期事件比較合適。

import React from 'react'
import PropTypes from 'prop-types'

class Child extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      phone: ''
    }
  }

  componentDidMount () {
    this.setState({ ...this.state, phone: this.props.value })
  }

  componentDidUpdate (prevProps) {
    if (this.props.value !== prevProps.value) {
      // eslint-disable-next-line react/no-did-update-set-state
      this.setState({ ...this.state, phone: this.props.value })
    }
  }

  handlePhoneChange = e => {
    // 當 input 被修改時,透過 onChange prop 通知父組件
    this.props.onChange(e.target.value)

    // 當 input 被修改時,同步異動 phone state 值
    this.setState({ ...this.state, phone: e.target.value })
  }

  render () {
    const { value } = this.props
    const { phone } = this.state

    return (
      <>
        <div>變動資料可通知父層組件來同步資料</div>
        <div><input type='text' value={phone} onChange={this.handlePhoneChange} /></div>
        <div>
          外部傳入的數值(value prop): {value} <br />
          內部使用的數值(phone state): {phone}
        </div>
      </>
    )
  }
}

Child.propTypes = {
  value: PropTypes.string,
  onChange: PropTypes.func
}

export default Child

 

 

也嚐嚐 Hook 的實作方式吧!


試看看以 Hook 方式開發的子組件,功能與需求完全相同,但語法看起來確實是精簡直覺多了,以此方式實作亦是不錯的選項之一,如果不熟悉 Hook 的話可以參考筆者 透過實例熟悉 Effect Hook 操作技巧 文章有進一步的說明。

import React, { useState, useEffect } from 'react'
import PropTypes from 'prop-types'

const Child = ({ value, onChange }) => {
  // phone state
  const [phone, setPhone] = useState('')

  // 將 value prop 同步異動子組件的 phone state 值
  useEffect(() => {
    setPhone(value)
  }, [value])

  const handlePhoneChange = e => {
    // 當 input 被修改時,透過 onChange prop 通知父組件
    onChange(e.target.value)

    // 當 input 被修改時,同步異動 phone state 值
    setPhone(e.target.value)
  }

  return (
    <>
      <div>
        <div>變動資料可通知父層組件來同步資料</div>
        <div><input type='text' value={phone} onChange={handlePhoneChange} /></div>
        <div>
          外部傳入的數值(value prop): {value} <br />
          內部使用的數值(phone state): {phone}
        </div>
      </div>
    </>
  )
}

Child.propTypes = {
  value: PropTypes.string,
  onChange: PropTypes.func
}


export default Child

 

 


希望此篇文章可以幫助到需要的人

若內容有誤或有其他建議請不吝留言給筆者喔 !