React中componentWillReceiveProps的替代升級方案


因為最近在做一個邏輯較為復雜的需求,在封裝組件時經常遇到父組件props更新來觸發子組件的state這種情景。在使用componentWillReceiveProps時,發現React官網已經把componentWillReceiveProps重名為UNSAFE_componentWillReceiveProps,但是我發現了getDerivedStateFromProps可以替代,卻又被一篇博客告知這個也盡量別使用。因為組件一旦使用派生狀態,很有可能因為沒有明確的數據來源導致出現一些bug和不一致性。既然提倡避免使用,肯定也會有相應的解決方案。

本文會介紹以上兩種生命周期的使用方法、誤區和替代升級方案。

componentWillReceiveProps

1.介紹

componentWillReceiveProps是React生命周期函數之一,在初始props不會被調用,它會在組件接受到新的props時調用。一般用於父組件更新狀態時子組件的重新渲染。在react16.3之前,componentWillReceiveProps是在不進行額外render的前提下,響應props中的改變並更新state的唯一方式。

2.使用方法

  componentWillReceiveProps(nextProps) { //通過this.props來獲取舊的外部狀態,初始 props 不會被調用  //通過對比新舊狀態,來判斷是否執行如this.setState及其他方法  } 

 

主要在以下兩種情景使用:

  • 從上傳的props無條件的更新state
  • 當props和state不匹配時候更新state

3.常見誤區
無條件的更新state

class EmailInput extends Component { state = { email: this.props.email }; componentWillReceiveProps(nextProps) { // 這樣會覆蓋內部 email的更新!  this.setState({ email: nextProps.email }); } handleChange = event => { this.setState({ email: event.target.value }); }; render() { return <input onChange={this.handleChange} value={this.state.email} />; } } 

 

從上述例子可以發現子組件的更新會被父組件的更新覆蓋。並且大家在使用過程沒有必要這樣無條件更新,完全可以寫成一個完全受控組件。

<input onChange={this.props.onChange} value={this.props.email} /> 

 

也可以對比新舊props狀態:

  componentWillReceiveProps(nextProps) { if (nextProps.email !== this.props.email) { this.setState({ email: nextProps.email }); } } 

 

現在該組件只會在props改變時候覆蓋之前的修改了,但是仍然存在一個小問題。例如一個密碼管理網站使用了如上的輸入組件。當切換兩個不同的賬號的時候,如果這兩個賬號的郵箱相同,那么我們的重置就會失效。因為對於這兩個賬戶傳入的email屬性是一樣的,即數據源相同。效果如下:

 

 

// 父組件: import React, { Component, Fragment } from 'react'; import { Radio } from 'antd'; import UncontrolledInput from './UncontrolledInput'; const accounts = [ { id: 1, name: 'One', email: 'same.email@example.com', }, { id: 2, name: 'Two', email: 'same.email@example.com', }, { id: 3, name: 'Three', email: 'other.fake.email@example.com', } ]; export default class AccountList extends Component { state = { selectedIndex: 0 }; render() { const { selectedIndex } = this.state; return ( <Fragment> <UncontrolledInput email={accounts[selectedIndex].email} /> <Radio.Group onChange={(e) => this.setState({ selectedIndex: e.target.value })} value={selectedIndex}> {accounts.map((account, index) => ( <Radio value={index}> {account.name} </Radio> ))} </Radio.Group> </Fragment> ); } } //子組件 import React, { Component } from 'react'; import { Input } from 'antd'; export default class UncontrolledInput extends Component { state = { email: this.props.email }; componentWillReceiveProps(nextProps) { if (nextProps.email !== this.props.email) { this.setState({ email: nextProps.email }); } } handleChange = event => { this.setState({ email: event.target.value }); }; render() { return ( <div> Email: <Input onChange={this.handleChange} value={this.state.email} /> </div> ); } } 

 

從id為1的賬戶切換到id為2的賬戶,因為傳入的email相同(nextProps.email === this.props.email),輸入框無法重置。從id為2的賬戶切換到id為3的賬戶,因為傳入的email不同,進行了輸入框的重置。大家可能想到,既然需要切換賬戶就重置,那就把id或者selectedIndex選中項作為判斷重置條件。

//父組件引入子組件方式 <UncontrolledInput email={accounts[selectedIndex].email} selectedIndex={selectedIndex} /> //子組件  componentWillReceiveProps(nextProps) { if (nextProps.selectedIndex !== this.props.selectedIndex) { this.setState({ email: nextProps.email }); } } 

 

 

 

其實當使用唯一標識符來判來保證子組件有一個明確的數據來源時,我們使用key是獲取是最合適的方法。並且不需要使用componentWillReceiveProps,只需要保證每次我們每次需要重置輸入框時候可以有不同的key值。

//父組件 <UncontrolledInput email={accounts[selectedIndex].email} key={selectedIndex} /> 

 

每當key發生變化,會引起子組件的重新構建而不是更新。當我們切換賬戶,不再是子組件而是重新構建,同樣的達到了重置的效果。但是還有一個小問題,當我們在一個賬戶做了更改之后,切換到其他賬戶並切換回來,發現我們的之前的更改不會緩存。這里我們可以將輸入框設計為一個完全可控組件,將更改的狀態存在父組件中。

//父組件 export default class Release extends Component { state = { selectedIndex: 0, valueList: [...accounts] }; onChange = (event) => { const { valueList, selectedIndex } = this.state; valueList[selectedIndex].email = event.target.value; this.setState({ valueList: [...valueList] }); } render() { const { selectedIndex, valueList } = this.state; return ( <Fragment> <UncontrolledInput email={valueList[selectedIndex].email} onChange={this.onChange} /> <Radio.Group onChange={(e) => this.setState({ selectedIndex: e.target.value })} value={selectedIndex}> {valueList.map((account, index) => ( <Radio value={index}> {account.name} </Radio> ))} </Radio.Group> </Fragment> ); } } //子組件 export default class UncontrolledInput extends Component { state = { email: this.props.email }; render() { return ( <div> Email: <Input onChange={this.props.onChange} value={this.props.email} /> </div> ); } }

 

替換方案:getDerivedStateFromProps

1.介紹

React在版本16.3之后,引入了新的生命周期函數getDerivedStateFromProps 需要注意的一點,在React 16.4^ 版本中getDerivedStateFromProps 比 16.3 版本中多了setState forceUpdate 兩種觸發方法。

 

 

 

 

詳情請看官方給出的生命周期圖。

2.使用

static getDerivedStateFromProps(nextProps,prevState){ //該方法內禁止訪問this  if(nextProps.email !== prevState.email){ //通過對比nextProps和prevState,返回一個用於更新狀態的對象  return { value:nextProps.email } } //不需要更新狀態,返回null  return null } 

 

如果大家仍需要通過this.props來做一些事,可以使用componentDidUpdate

componentDidUpdate(prevProps, prevState, snapshot){ if(this.props.email){ // 做一些需要this.props的事  } } 

 

通過以上使用方法,React相當於把componentWillReceiveProps拆分成getDerivedStateFromProps和componentDidUpdate。拆分后,使得派生狀態更加容易預測。

3.常見誤區

當我們在子組件內使用該方法來判斷新props和state時,可能會引起內部更新無效。

//子組件 export default class UncontrolledInput extends Component { state = { email: this.props.email }; static getDerivedStateFromProps(props, state) { if (props.email !== state.email) { return { email: props.email }; } return null; } handleChange = event => { this.setState({ email: this.props.email }); }; render() { return ( <div> Email: <Input onChange={this.handleChange} value={this.state.email} style={style} /> </div> ); } } 

 

這是因為在 React 16.4^ 的版本中,setState 和 forceUpdate 也會觸發getDerivedStateFromProps方法。當我們嘗試改變輸入框值,觸發setState方法,進而觸發該方法,並把 state 值更新為傳入的 props。雖然在getDerivedStateFromProps中,不能訪問this.props,但是我們可以新加個字段來間接訪問this.props進而判斷新舊props。

export default class UncontrolledInput extends Component { state = { email: this.props.email, prevPropEmail: '' }; static getDerivedStateFromProps(props, state) { if (props.email !== state.prevPropEmail) { return { email: props.email, prevPropEmail: props.email, }; } return null; } handleChange = event => { this.setState({ email: this.props.email }); }; render() { return ( <div> Email: <Input onChange={this.handleChange} value={this.state.email} style={style} /> </div> ); } } 

 

通過上一個props.email來判斷是否更新,而不是通過state的狀態。雖然解決了內部更新問題,但是並不能解決componentWillReceiveProps中提到的多個賬戶切換無法重置等問題。並且這樣寫的派生狀態代碼冗余,並使組件難以維護。

升級方案

我們在開發過程中很難保證每個數據都有明確的數據來源,盡量避免使用這兩個生命周期函數。結合以上例子以及官網提供的方法,我們有以下升級方案: 1.完全受控組件(推薦) 2.key標識的完全不可控組件(推薦) 使用React的key屬性。通過傳入不同的key來重新構建組件。在極少情況,你可能需要在沒有合適的ID作為key的情況下重置state,可以將需要重置的組件的key重新賦值為當前時間戳。雖然重新創建組件聽上去會很慢,但其實對性能的影響微乎其微。並且如果組件具有很多更新上的邏輯,使用key甚至可以更快,因為該子樹的diff得以被繞過。 3.通過唯一屬性值重置非受控組件。 因為使用key值我們會重置子組件所有狀態,當我們需要僅重置某些字段時或者子組件初始化代價很大時,可以通過判斷唯一屬性是否更改來保證重置組件內部狀態的靈活性。 4.使用實例方法重置非受控組件。 當我們沒有合適的特殊屬性去匹配的時候,可以通過實例方法強制重置內部狀態

 //父組件  handleChange = index => { this.setState({ selectedIndex: index }, () => { const selectedAccount = accounts[index]; this.inputRef.current.resetEmailForNewUser(selectedAccount.email); }); }; //子組件  resetEmailForNewUser(defaultEmail) { this.setState({ email: defaultEmail }); } 

 

總結

升級方案不僅僅以上幾種,例如當我們僅僅需要當props更改進行數據提取或者動畫時,可以使用componentDidUpdate。還可以參考官網提供的memoization(緩存記憶)。但是主要推薦的方案是完全受控組件和key值的完全不受控組件。當無法滿足需求的特殊情況,再使用其他方法。總之,componentWillReceiveProps/getDerivedStateFromProps是一個擁有一定復雜度的高級特性,我們應該謹慎地使用。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM