1. 程式人生 > 其它 >React中componentWillReceiveProps的替代升級方案

React中componentWillReceiveProps的替代升級方案

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: '[email protected]',
},
{
id: 2,
name: 'Two',
email: '[email protected]',
},
{
id: 3,
name: 'Three',
email: '[email protected]',
}
];
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是一個擁有一定複雜度的高階特性,我們應該謹慎地使用。

歡迎關注前端早茶,與廣東靚仔攜手共同進階

前端早茶專注前端,一起結伴同行,緊跟業界發展步伐~

公眾號作者:廣東靚仔