Fetch() and setState() update timing trouble

Hello!

I’m working on a solo project using React and I’ve been stuck on something for the past 2 days… :frowning:

To try to be concise and clear:

It’s a bit like the Ravenous project. I have a searchBar component, that search through a local database, and returned object associated with the search keyword. Nothing complicated so far.
Each rendered object has a button that triggers a function onClick. The said function is a follow:

changeState(term){
    let idToRender=[];
    this.state.dealersDb.map(dealer=>{
    if(term===dealer.id){
        idToRender=[dealer];
        }});
    let recoToFind=idToRender[0].reco;
    recoToFind.map(item=>{
        Discogs.search(item).then(response=>{idToRender[0].recoInfo.push(response)})
    })
    
    this.setState({
    objectToRender: idToRender
    });

to explain the above code, what it does is that first, it identifies which object’s button has been clicked on, and send said object to a variable called idToRender. Then, it takes the reco state of that object, and store it to another variable called recoToFind. Then it calls the map() method on recoToFind, make an API request (the discogs() method) for each element of the recoToFind array and push() the results into the recoInfo state of idToRender. So by the end of the function, idToRender is supposed to look like this:
[{


recoInfo: [{1stAPI call result},{2ndAPI call result}…]
}],

The array contains 1 object having all the states of the object that was originally clicked on, plus a state recoInfo equal to an array made of the results of the several API calls.
Finally, it updates the component’s state objectToRender to idToRender.

And here my problem is, onClick, I do get all the states values of the clicked on object that get rendered on screen (as expected with how I coded the nested components), BUT, the values of the recoInfo are not displayed as expected (The component who’s supposed to render those values is nested in the component rendering the clicked on object other states values). However, they get displayed properly after a SECOND click on the button. So it seems my problem boils down to an update timing trouble, but I’m puzzled, cause this function is calling setState once and I know for a fact that the state is updated cause when I click on the button, the clicked on Object details get displayed, but somehow the recoInfo state seems to not be available yet, but only becomes available on a second click…

Would anyone have a way to solve this issue? :frowning:
It somehow feels like my salvation lies in async/await, but I’m not sure I understand them correctly…

thanks very much in advance for any help!

I can’t verify this because you’re not giving me something I can reproduce and I don’t really know what’s going on - but updating state is something you’d do as part of handling the response, which won’t happen until after you’ve exited the current function (you have to yield before other events can run)

Since you have many requests, you have to wait for all of them. You have a list of requests and you want to turn that into something that looks like a request for a list - you could write a function that does this, or maybe there’s some library function from that in the same library that (because that’s a pretty generic thing, but I have no clue what’s in that ecosystem), but making the function is probably interesting and not that difficult as long as you keep in mind what’s going on.

I suppose simpler yet would be to update once on each response, not sure I like the idea but I won’t deny that it would at least be correct. (With the downside of causing many re-renderings)

Thank you ionatan!

So I’ve tried calling the setState on each response, and indeed, it worked, but causes many re-rendering thus adding a little delay on the rendering of the final result. But, although it does work, I don’t understand why the state is updated correctly when I call the setState on each response, but is not updated correctly when I call setState in the same function, but only once, with the object composed of the results from the various api calls. Both setState are called within my changeState() method, one way work as expected but the other doesn’t… Could you try to give me some keys for a better understanding of this process? It really feels like I’m missing something here…

btw, here’s the whole code for the component where changeState() is declared, as well as the Discogs() method:

import React from 'react';
import LogoHeartbeat from './heartbeat_logo.png';
import LogoLoveVinyl from './lvlogo.jpg';
import SearchBarDealers from '../SearchBarDealers/SearchBarDealers.js';
import SearchDealersResults from '../SearchDealersResults/SearchDealersResults';
import DealerDetailsList from '../DealerDetailsList/DealerDetailsList';
import Discogs from '../../util/Discogs/Discogs';
import './Dealers.css';


class Dealers extends React.Component{
    constructor(props){
        super(props);
        this.state = {
            recoDetails:[],
            objectToRender:[],
            searchResults:[],
            dealersDb:[
            {
                name: 'Heartbeat Vinyl',
                location: 'Paris',
                address: '26 Rue Godefroy Cavaignac',
                open: 'from 12 pm to 8pm',
                holiday: 'Sunday',
                description:"Heartbeat Vinyl is a very cosy shop, offering a wide range of music. You'll find Jazz, Disco, House, Techno, Rock, Ambient, Soul, Funk, etc... One thing is sure, you can expect quality music from the owner Melik!",
                url: 'http://www.heartbeatvinyl.com/',
                logo: LogoHeartbeat,
                genre: ['Jazz','Rock','Disco','Funk','House','Japanese','Techno','Ambient'],
                id:'1',
                reco:['15179807','3447244','1559040'],
                recoInfo:[]
            },
            {
                name: 'Love Vinyl',
                location: 'London',
                address: '5 Pearson Street',
                open: 'from 11:30 am to 7pm',
                holiday: 'Monday',
                description:"Love Vinyl is an incredibly good record shop, offering a wide range of music. You'll find Jazz, Disco, House, Techno, Rock, Ambient, Soul, Funk, etc... One thing is sure, you can expect quality music from the owner Zaf!",
                url: 'https://www.lovevinyl.london/',
                logo: LogoLoveVinyl,
                genre: ['Jazz','Rock','Disco','Funk','House','Techno','Punk','Mods'],
                id:'2',
                reco:['2903013','1038746','13670323'],
                recoInfo:[]
            }
        ]
    };
    this.search=this.search.bind(this);
    this.changeState=this.changeState.bind(this);
    this.getRecoDetails=this.getRecoDetails.bind(this);
}

search(term){
    this.setState({
        searchResults: []
    });
let tempArray= [];
this.state.dealersDb.map(dealer=>{
    if(term===dealer.name || term===dealer.location || dealer.genre.find(element=>term===element)){
    tempArray.push(dealer);
    this.setState({
        searchResults: tempArray
    })
    }
});
}


    changeState(term) {
        let idToRender=[];
        this.state.dealersDb.map(dealer=>{
        if(term===dealer.id){
            idToRender=[dealer];
            }});
        let recoToFind=idToRender[0].reco;
        recoToFind.map(item=>{
            Discogs.search(item).then(response=>{
                idToRender[0].recoInfo.push(response)
                this.setState({
                    objectToRender: idToRender
                });
            })
        }) 
}

getRecoDetails(id){
    Discogs.search(id).then(response=>{
        this.state.recoDetails.push(response)
        return(
        this.setState({recoDetails: this.state.recoDetails
       }))});
    }



render(){
        return(
    <div>
        <div className="OriginalComponents">
        <SearchBarDealers onSearch={this.search}/>
        <div className="MainContainer">
            <div className="SDResults">
        <SearchDealersResults data={this.state.searchResults} getId={this.changeState}/>
        </div>
        <div className="DDList">
        <DealerDetailsList show={this.state.objectToRender}/>
        </div>
        </div> 
        </div>
    </div>
    
        );
    }
}

export default Dealers;

as you can see, I have declared a getRecoDetails() method that I’m not using anymore, but it’s a trace of my past attempt to isolate the function for the API calls, but I would always end up in either infinite loops, or results displayed x times where x is the number of re-rendering basically…

Here’s the discogs method:

const apiKey='hidden for privacy';
const secret='hidden for privacy';

let Discogs = {
search(id){
return fetch(`https://api.discogs.com/releases/${id}?key=${apiKey}&secret=${secret}`,{
    header:{
        'User-Agent':'MyRecordDealer/0.1+http://localhost:3000/Dealers'
    }
}).then(response=>{
   return response.json()
}).then(jsonResponse=>{
    if(jsonResponse){
        return ({
            title: jsonResponse.title,
            artist: jsonResponse.artists_sort,
            thumbnail: jsonResponse.thumb,
        })
    }
})
    }
};

export default Discogs;

Your function currently has control over the program. The handler isn’t allowed to run until your function gives up that control so that the event loop can run new things, such as your handlers. It doesn’t matter whether the responses have arrived, the handlers are blocked from running.

And yeah implement a function that does:

Array<Promise>  -->  Promise<Array>

It’s a trivial function. If you have an array of promises, then you should be able to create an array with all the resolved results, which will necessarily be wrapped inside a promise as a result of having to wait on other promises.

async function sequence(promises) {
    const results = []
    ... // what would you need to do here?
    return results
}

I have no clue how failed requests tie into this, but I suppose that’s a separate problem anyway.

You also have a problem of making many requests. Like making many re-renders, making many requests could be considered expensive for the client and/or server. Better if you make one combined request. (Similar to how you should be able to turn an array of promises into a promise of an array, you should be able to turn many requests into request of many)

Maybe this will help from Dave’s book Pure React.

"If you call setState and immediately console this.state right afterwards, it will very likely print the old state instead of the one you just set…

If you need to set the state and immediately act on that change, you can pass the callback function as the last argument to setState, like this:"

this.setState({name: 'Joe'}, function() {
  // called after state has been updated
  // and the component has been re-rendered
  // this.state now contains { name: 'Joe' }
});

“Another way to make it so that sequential state updates run in sequence is to use the functional form of setState, like this [example of a counter component]:”

this.setState((state, props) => {
  return {
    value: state.value + 1
  }
});

Excerpts From: Dave Ceddia. “Pure React.” Apple Books.

Ionatan>

Thanks for the explanation. To be honest, I’m still not 100% clear on this…But at least I have a solution that works for now so I’ll just keep this on the side for later.

cloud7873894466>

Thank you for this!
I think I understood the part where this.setState doesn’t update the state immediately. I believe my fundamental non-understanding is about the fact that, with my method:

changeState(term){
    let idToRender=[];
    this.state.dealersDb.map(dealer=>{
    if(term===dealer.id){
        idToRender=[dealer];
        }});
    let recoToFind=idToRender[0].reco;
    recoToFind.map(item=>{
        Discogs.search(item).then(response=>{idToRender[0].recoInfo.push(response)})
    })
    
    this.setState({
    objectToRender: idToRender
    });

the objectToRender state IS updated with the new object (idToRender), but one state of this new object, namely idToRender[0].recoInfo seem to not be updated properly.
But with the fix, which calls setState everytime something is pushed into recoInfo, somehow it gets updated properly. Basically what I don’t understand is why:

push(A)
setState
push(B)
setState
push©
setstate

works, but
push(A)
push(B)
push©
setState

doesn’t. Because if it was a problem with the fact that setState doesn’t get updated immediately, then with this method:

push(A)
setState
push(B)
setState
push©
setstate

I guess C shouldn’t be available in the array, right?

What React priority rule results in this? :frowning: