[React] componentWillReceiveProp
1. 背景
父组件异步获取数据,传递给子组件,子组件在这些数据中进行选择。
当选项发生改变的时候,父组件能根据选项做出响应。
子组件要默认选中第一个。
而且,从开始装载到默认选中第一个,也视为选项发生了改变。
2. 问题
2.1 父组件
它render了以下子组件,
<NumberSelector numbers={numbers} onChange={onNumberSelectorChange} />
其中,numbers
来自父组件state
,初始值为空数组[ ]
,
componentDidMount
通过ajax异步取值后,修改state
。
onNumberSelectorChange
的定义如下,
onNumberSelectorChange = number => this.setState({
selectedNumber: number,
});
它会在子组件onChange
事件发生后,修改父组件的state
。
2.2 子组件
由于父组件componentDidMount
中的ajax返回后,numbers
才有值,
所以,子组件在装载时,接受到的numbers
为父组件的默认值[ ]
。
父组件ajax返回后更新state
,会重新render,
从而导致子组件更新。
因此,在子组件componentWillReceiveProps
中,
才可以接到ajax返回后的numbers
值。
componentWillReceiveProps = ({ numbers, onChange }) => {
const {
state: { selectedNumber },
} = this;
const firstNumber = numbers[0];
this.setState({
selectedNumber: firstNumber,
});
onChange(firstNumber);
};
2.3 结果
父组件的componentDidMount
会抛异常:
> Uncaught (in promise) RangeError: Maximum call stack size exceeded
3. 原因分析
子组件的componentWillReceiveProps
函数陷入了死循环。
在此函数中,子组件使用onChange(firstNumber);
向父组件传值,
父组件通过onNumberSelectorChange
改变自身的state
。
由于React在render时,
不管子组件属性是否改变,都会调用子组件的componentWillReceiveProps。
componentWillReceiveProps :
"Note that React may call this method even if the props have not changed...
因此,父组件改变了自身state
后,即使子组件的属性没有变化,
也会触发componentWillReceiveProps
。
因此,子组件在componentWillReceiveProps
中,
调用onChange
更改父组件的state
,
会引发子组件的componentWillReceiveProps
再次被调用,导致死循环。
最终调用栈溢出。
4. 解决方案
componentWillReceiveProps
中可以更改父组件状态,
但是要增加判断条件,避免陷入死循环。
// 设置默认选中第一项
componentWillReceiveProps = ({ numbers, onChange }) => {
const {
state: { selectedNumber },
} = this;
// 如果numbers清空了,且内部有状态,就清空状态,触发onChange
if (numbers.length === 0 && selectedNumber != null) {
this.setState({
selectedNumber: null,
});
// 向父组件传null值
onChange(null);
return;
}
// 注:终止条件 1
// 如果numbers清空了,且内部无状态,则不触发onChange
if (numbers.length === 0) {
return;
}
// 注:终止条件 2
// 如果selectedNumber在numbers中,就不改变它,直接返回
const isContainedInNumbers = numbers.some(number => number === selectedNumber);
if (isContainedInNumbers) {
return;
}
// 否则,设置为选中第一项
const firstNumber = numbers[0];
this.setState({
selectedNumber: firstNumber,
});
// 由于onClick会更新父组件state,导致父组件重新render,
// 而React在render时,不管子组件属性是否改变,都会调用componentWillReceiveProps,
// 因此,onClick可能会导致componentWillReceiveProps死循环
// 不过没关系,我们前面加上了终止条件
onChange(firstNumber);
};
以上代码新增了isContainedInNumbers
判断,
从而可以在selectedNumber
被设置后,
避免连续触发componentWillReceiveProps
。
参考
React Docs - componentWillReceiveProps()
Github: thzt/receive-props-infinite-loop