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

184
February 06, 2022, at 01:30 AM

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>

Answer 1

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.

Rent Charter Buses Company
READ ALSO
addEventListener for submits overrides all my submit functions

addEventListener for submits overrides all my submit functions

I have an infinite scrolling template that has logic to override the submit action of formsThe issue is when my loop runs through it attaches the last eventListener to all my submit forms which causes issues

31
Why is this class in PHP working so weirdly with interface? [closed]

Why is this class in PHP working so weirdly with interface? [closed]

Want to improve this question? Add details and clarify the problem by editing this post

103
What is a Python Flask alternative to a JS &quot;onclick&quot; script?

What is a Python Flask alternative to a JS "onclick" script?

I want to make a Flask application for a puzzleUser should find some elements on a picture and click on them

106