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