[React] componentWillReceiveProps引起的死循環(huán)

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 原教程內(nèi)容詳見精益 React 學(xué)習(xí)指南,這只是我在學(xué)習(xí)過程中的一些閱讀筆記,個(gè)人覺得該教程講解深入淺出,比目前大...
    leonaxiong閱讀 2,854評(píng)論 1 18
  • 自己最近的項(xiàng)目是基于react的,于是讀了一遍react的文檔,做了一些記錄(除了REFERENCE部分還沒開始讀...
    潘逸飛閱讀 3,455評(píng)論 1 10
  • 目前,react組件有三種寫法,分別是es5的createClass寫法,es6的class寫法,以及statel...
    ZoomFunc閱讀 1,761評(píng)論 0 1
  • 最近看了一本關(guān)于學(xué)習(xí)方法論的書,強(qiáng)調(diào)了記筆記和堅(jiān)持的重要性。這幾天也剛好在學(xué)習(xí)React,所以我打算每天堅(jiān)持一篇R...
    gaoer1938閱讀 1,712評(píng)論 0 5
  • 一:一天二十四個(gè)小時(shí),我們每天花多少時(shí)間在手機(jī)上,你計(jì)算過么?早上睡醒,有沒有習(xí)慣拿起手機(jī),中午吃飯的時(shí)候會(huì)不會(huì)掏...
    咩的一聲羊叫閱讀 525評(píng)論 0 2