简体   繁体   中英

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

Pretty new to React. I'm having some problems rendering a button component. What I'm trying to do is to create a button that, when clicked, fetches some data and displays it under the button itself as a list. To do so, I'm trying to do a conditional rendering. I used the state of the button component as the number of data fetched, initialized to zero. So the first time I would only render the button and not try to render the list at all. When the button gets clicked, the onClick event executes the fetch, getting the data. At this point, the state should be updated, but if I call setState() to update it, of course React advises me with a warning that I'm creating a infinite loop (I'm calling setState() inside a render() function after all).

The most common lifecycle components are not helping me, since when the component gets mounted the user has not yet pressed the button (can't use componentDidMount() then), and if I remove the setState() from the onClick function the component does not update, so I'm out of methods to call setState() from. And given that a component changing its props by itself is anti-pattern, I'm out of ideas.

Here is the code:

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> 
            )
        }
        
    }

}

Stack Snippet:

 <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>

I solved the problem by editing the < MapList > and < MapEntry > components that I didn't disclose. Spoiler: just some wrong assignments from props to state and some "grammatically" wrong returns in the render of those two components. I was deceived by the warning that VisualStudio Code was throwing, which was this one (and I gotta say it's still there after I fixed the code the list is then rendered):

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

Debugging the project, I had some breakpoints at the beginning of MapList.js and MapEntry.js files that were never reached during code execution: that deceived me into thinking I made a mistake in the render of the button. What I meant by "calling setState() into a render()" was that I associated the async function fetchHaloMaps() (which called the setState() at the end of it) as the onClick handler of the button defined in the render(). But, after solving the problem, the explanation seems obvious: the button is rendered and the function is not called in the process of rendering, it gets called by the user after the button is already rendered - there's no infinite loop of rendering at all, that segment I posted does what I intended and my question now looks very stupid:)

By the way, I still can't figure out why does that warning appear and how may it impact the app.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM