1. 背景
父組件異步獲取數(shù)據(jù),傳遞給子組件,子組件在這些數(shù)據(jù)中進行選擇。
當(dāng)選項發(fā)生改變的時候,父組件能根據(jù)選項做出響應(yīng)。
子組件要默認選中第一個。
而且,從開始裝載到默認選中第一個,也視為選項發(fā)生了改變。
2. 問題
2.1 父組件
它render了以下子組件,
<NumberSelector numbers={numbers} onChange={onNumberSelectorChange} />
其中,numbers來自父組件state,初始值為空數(shù)組[ ],
componentDidMount通過ajax異步取值后,修改state。
onNumberSelectorChange的定義如下,
onNumberSelectorChange = number => this.setState({
selectedNumber: number,
});
它會在子組件onChange事件發(fā)生后,修改父組件的state。
2.2 子組件
由于父組件componentDidMount中的ajax返回后,numbers才有值,
所以,子組件在裝載時,接受到的numbers為父組件的默認值[ ]。
父組件ajax返回后更新state,會重新render,
從而導(dǎo)致子組件更新。
因此,在子組件componentWillReceiveProps中,
才可以接到ajax返回后的numbers值。
componentWillReceiveProps = ({ numbers, onChange }) => {
const {
state: { selectedNumber },
} = this;
const firstNumber = numbers[0];
this.setState({
selectedNumber: firstNumber,
});
onChange(firstNumber);
};
2.3 結(jié)果
父組件的componentDidMount會拋異常:
> Uncaught (in promise) RangeError: Maximum call stack size exceeded
3. 原因分析
子組件的componentWillReceiveProps函數(shù)陷入了死循環(huán)。
在此函數(shù)中,子組件使用onChange(firstNumber);向父組件傳值,
父組件通過onNumberSelectorChange改變自身的state。
由于React在render時,
不管子組件屬性是否改變,都會調(diào)用子組件的componentWillReceiveProps。
componentWillReceiveProps :
"Note that React may call this method even if the props have not changed...
因此,父組件改變了自身state后,即使子組件的屬性沒有變化,
也會觸發(fā)componentWillReceiveProps。
因此,子組件在componentWillReceiveProps中,
調(diào)用onChange更改父組件的state,
會引發(fā)子組件的componentWillReceiveProps再次被調(diào)用,導(dǎo)致死循環(huán)。
最終調(diào)用棧溢出。
4. 解決方案
componentWillReceiveProps中可以更改父組件狀態(tài),
但是要增加判斷條件,避免陷入死循環(huán)。
// 設(shè)置默認選中第一項
componentWillReceiveProps = ({ numbers, onChange }) => {
const {
state: { selectedNumber },
} = this;
// 如果numbers清空了,且內(nèi)部有狀態(tài),就清空狀態(tài),觸發(fā)onChange
if (numbers.length === 0 && selectedNumber != null) {
this.setState({
selectedNumber: null,
});
// 向父組件傳null值
onChange(null);
return;
}
// 注:終止條件 1
// 如果numbers清空了,且內(nèi)部無狀態(tài),則不觸發(fā)onChange
if (numbers.length === 0) {
return;
}
// 注:終止條件 2
// 如果selectedNumber在numbers中,就不改變它,直接返回
const isContainedInNumbers = numbers.some(number => number === selectedNumber);
if (isContainedInNumbers) {
return;
}
// 否則,設(shè)置為選中第一項
const firstNumber = numbers[0];
this.setState({
selectedNumber: firstNumber,
});
// 由于onClick會更新父組件state,導(dǎo)致父組件重新render,
// 而React在render時,不管子組件屬性是否改變,都會調(diào)用componentWillReceiveProps,
// 因此,onClick可能會導(dǎo)致componentWillReceiveProps死循環(huán)
// 不過沒關(guān)系,我們前面加上了終止條件
onChange(firstNumber);
};
以上代碼新增了isContainedInNumbers判斷,
從而可以在selectedNumber被設(shè)置后,
避免連續(xù)觸發(fā)componentWillReceiveProps。
參考
React Docs - componentWillReceiveProps()
Github: thzt/receive-props-infinite-loop