反应无限循环 - 渲染调用 setState() 中的 onClick

React infinite loop - onClick inside a render calls setState()

React 的新手。我在渲染按钮组件时遇到了一些问题。我想要做的是创建一个按钮,当单击该按钮时,它会获取一些数据并将其作为列表显示在按钮本身下方。为此,我正在尝试进行条件渲染。 我使用按钮组件的状态作为获取的数据数,初始化为零。所以第一次我只会渲染按钮而不尝试渲染列表。单击按钮时,onClick 事件执行提取,获取数据。在这一点上,状态应该被更新,但是如果我调用 setState() 来更新它,React 当然会警告我我正在创建一个无限循环(我在 render() 中调用 setState()毕竟功能)。

最常见的生命周期组件对我没有帮助,因为当组件被安装时用户还没有按下按钮(那时不能使用 componentDidMount()),如果我从onClick 函数组件不会更新,所以我没有方法可以从中调用 setState() 。考虑到一个组件自己改变它的 props 是反模式的,我没主意了。

代码如下:

import { MapList } from './MapList';

export class ButtonFetcher extends React.Component
{

    constructor(props)
    {
        super(props);

        this.state = { numberOfMaps: 0 };

        this.mapArray = [];

        this.fetchHaloMaps = this.fetchHaloMaps.bind(this);
    }

    async fetchHaloMaps()
    {
        const url = 'https://cryptum.halodotapi.com/games/hmcc/metadata/maps'   

        fetch(url, {
            "method": 'GET',
            "headers": {
                        'Content-Type': 'application/json',
                        'Cryptum-API-Version': '2.3-alpha',
                        'Authorization': 'Cryptum-Token XXX'
                     }
        })
        .then(response =>      
            response.json())   
        .then(res => {  
                    
            let d;
            let i=0;
            for (; i< res.data.length; i++)
            {
                d = res.data[i];
                this.mapArray[i] = d.name;
            }

            this.setState(({  
                numberOfMaps : i
            }));  
        })  
        .catch(error => {   
            console.log("There was an error: " + error);
        });
    }


    render()
    {
        if (this.state.numberOfMaps === 0)
        {
            return (
                <button type="button" onClick={this.fetchHaloMaps} >Retrieve giafra's last maps!</button>
            )
        }

        else
        {
            return (
                <div>
                    <button type="button" onClick={this.fetchHaloMaps} >Retrieve giafra's last maps!</button>
                    <MapList mapNames={this.mapArray} />
                </div> 
            )
        }
        
    }

}

堆栈片段:

<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
<script type="text/babel" data-presets="es2017,react,stage-3">
const { useState } = React;

// Promise-based delay function
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

// Stand-in for `MapList`
const MapList = ({mapNames}) => <ul>
    {mapNames.map(name => <li key={name}>{name}</li>)}
</ul>;

/*export*/ class ButtonFetcher extends React.Component
{
    constructor(props)
    {
        super(props);
        this.state = { numberOfMaps: 0 };
        this.mapArray = [];
        this.fetchHaloMaps = this.fetchHaloMaps.bind(this);
    }

    async fetchHaloMaps()
    {
        const url = 'https://cryptum.halodotapi.com/games/hmcc/metadata/maps'   

        /*
        fetch(url, {
            "method": 'GET',
            "headers": {
                        'Content-Type': 'application/json',
                        'Cryptum-API-Version': '2.3-alpha',
                        'Authorization': 'Cryptum-Token XXX'
                     }
        })
        .then(response =>      
            response.json())   
        */
        delay(800)                  // ***
        .then(() => ({              // ***
            data: [                 // ***
                {name: "one"},      // *** A stand-in for the fetch
                {name: "two"},      // ***
                {name: "three"},    // ***
            ]                       // ***
        }))                         // ***
        .then(res => {  
            let d;
            let i=0;
            for (; i< res.data.length; i++)
            {
                d = res.data[i];
                this.mapArray[i] = d.name;
            }

            this.setState(({  
                numberOfMaps : i
            }));  
        })  
        .catch(error => {   
            console.log("There was an error: " + error);
        });
    }

    render()
    {
        if (this.state.numberOfMaps === 0)
        {
            return (
                <button type="button" onClick={this.fetchHaloMaps} >Retrieve giafra's last maps!</button>
            );
        }

        else
        {
            return (
                <div>
                    <button type="button" onClick={this.fetchHaloMaps} >Retrieve giafra's last maps!</button>
                    <MapList mapNames={this.mapArray} />
                </div> 
            );
        }
    }
}

ReactDOM.render(<ButtonFetcher />, document.getElementById("root"));
</script>
<script src="https://unpkg.com/regenerator-runtime@0.13.2/runtime.js"></script>
<script src="https://unpkg.com/@babel/standalone@7.10.3/babel.min.js"></script>

我通过编辑我没有透露的组件解决了这个问题。剧透:只是一些从 props 到 state 的错误分配,以及在这两个组件的渲染中一些“语法”错误 returns。我被 VisualStudio Code 抛出的警告所欺骗,就是这个警告(我必须说,在我修复代码后它仍然存在,然后呈现列表):

Warning: unstable_flushDiscreteUpdates: Cannot flush updates when React is already rendering. at ButtonFetcher (http://localhost:3000/static/js/bundle.js:108:5) at div at App

调试项目时,我在 MapList.js 和 MapEntry.js 文件的开头设置了一些断点,这些断点在代码执行期间从未到达:这让我误以为我在渲染时犯了一个错误按钮。我所说的“将 setState() 调用到 render()”的意思是我将异步函数 fetchHaloMaps()(在它的末尾调用了 setState())关联为 render( ).但是,在解决问题之后,解释似乎很明显:渲染按钮并且在渲染过程中没有调用该函数,而是在按钮已经渲染之后由用户调用 - 根本没有渲染的无限循环,我发布的那段内容符合我的预期,我的问题现在看起来很愚蠢 :)

顺便说一下,我仍然无法弄清楚为什么会出现该警告以及它对应用程序有何影响。