Object property coming up as undefined after assigning with an async fetch

546 Views Asked by At

I'm having some trouble with adding a key to an object as seen here:

    const recursiveFetchAndWait = useCallback(
        (url) => {
            setLoading(true);
    
            fetch(url)
                .then(async response => {
                    if (response.status === 200) { // Checking for response code 200
                        const xml = await response.text();
                        setLoading(false);
                        return XML2JS.parseString(xml, (err, result) => { // xml2js: converts XML to JSON
                            if (result.items.$.totalitems !== '0') { // Only processing further if there are returned results
                                result.items.item.forEach(game => {
                                    /* Fetching the statistics from a separate API, because the base API doesn't include these */
                                    const gameId = game.$.objectid;
                                    fetch('https://cors-anywhere.herokuapp.com/https://www.boardgamegeek.com/xmlapi2/thing?id=' + gameId + '&stats=1')
                                        .then(async response => {
                                            const xml = await response.text();
                                            return XML2JS.parseString(xml, (err, result) => {
                                                console.log('result', result); // This returns data.
                                                game.statistics = result.items.item[0].statistics[0].ratings[0];
                                                // setStatistics(...{statistics}, ...{gameId: result.items.item[0].statistics[0].ratings[0]})
                                            })
                                        })
    
                                    console.log('game', game); // This returns the object with the newly statistics key.
                                    console.log('STATS!', game.statistics); // This doesn't recognize the statistics key?!
    
                                    /* Going through the array and changing default values and converting string numbers to actual numbers */
                                    if (game.stats[0].rating[0].ranks[0].rank[0].$.value === 'Not Ranked')
                                        game.stats[0].rating[0].ranks[0].rank[0].$.value = 'N/A';
                                    else {
                                        game.stats[0].rating[0].ranks[0].rank[0].$.value = Number(game.stats[0].rating[0].ranks[0].rank[0].$.value);
                                    }
    
                                    game.stats[0].$.minplayers = Number(game.stats[0].$.minplayers);
                                    if (isNaN(game.stats[0].$.minplayers))
                                        game.stats[0].$.minplayers = '--';
    
                                    game.stats[0].$.maxplayers = Number(game.stats[0].$.maxplayers);
                                    if (isNaN(game.stats[0].$.maxplayers))
                                        game.stats[0].$.maxplayers = '--';
    
                                    game.stats[0].$.maxplaytime = Number(game.stats[0].$.maxplaytime);
                                    if (isNaN(game.stats[0].$.maxplaytime))
                                        game.stats[0].$.maxplaytime = '--';
    
                                    if (game.yearpublished === undefined)
                                        game.yearpublished = ['--'];
                                });
                                setGameList(result.items.item)
                            }
                        });
                    } else if (response.status === 202) { // If the status response was 202 (API still retrieving data), call the fetch again after a set timeout
                        setTimeoutAsCallback(() => recursiveFetchAndWait(url));
                    } else
                        console.log(response.status);
                })
        },
        [],
    );

Here are the results from the console.logs: image

I fear the issue relates with the async call, but I'm confused as to why the first console.log() works fine then. If it is an async issue, how do I go about resolving this?

3

There are 3 best solutions below

2
On BEST ANSWER

Your first console.log works because the "game" variable already exists and contains data before you even make the async fetch request. You could call it before the fetch and it would still be ok.

Your second console.log trying to output "game. statistics" is being run before the fetch has returned with any data. This is because async calls do not stop and wait for the code to complete the async task before moving on to the next lines of code. That is the intended purpose of an asynchronous code block. It will run the code inside the callback with the response once it's returned to perform anything that relies on the returned data. But without blocking the browser from continuing through the code to run the rest of the lines of code.

To achieve what you seem to be attempting to do, you could either place the task that you need to run after getting the data in a separate function and then calling it with the response.

games.forEach(game => {
    fetch('https://www.boardgamegeek.com/xmlapi2/thing?id='+game.$.objectid+'&stats=1')
    .then(response => {
        processData(response, game);
    })
});

const processData = (response, game) => {
    const xml = response.text(); 
    XML2JS.parseString(xml, (err, result) => { 
            game.statistics = result.items.item[0].statistics[0].ratings[0];
    })
    console.log('game', game);
    console.log('STATS!', game.statistics);
}

or you could explicitly tell it to wait for the completion of the async task to complete before moving on. This would require you to either use promises or wrap the entire games foreach loop in an async function. This is because only an async function knows how to handle processing an await on another async function called inside of itself.


Code for updated question

The editor wont let me format code properly anymore, but essentially the simplest solution is all your data handling logic should be executed within the XML callback. From your shared code I can't see any requirement for it to exist outside of the callback where the data is handled after it has been retrieved.

const recursiveFetchAndWait = useCallback(
        (url) => {
            setLoading(true);
    
            fetch(url)
                .then(async response => {
                    if (response.status === 200) { // Checking for response code 200
                        const xml = await response.text();
                        setLoading(false);
                        return XML2JS.parseString(xml, (err, result) => { // xml2js: converts XML to JSON
                            if (result.items.$.totalitems !== '0') { // Only processing further if there are returned results
                                result.items.item.forEach(game => {
                                    /* Fetching the statistics from a separate API, because the base API doesn't include these */
                                    const gameId = game.$.objectid;
                                    fetch('https://cors-anywhere.herokuapp.com/https://www.boardgamegeek.com/xmlapi2/thing?id=' + gameId + '&stats=1')
                                        .then(async response => {
                                            const xml = await response.text();
                                            return XML2JS.parseString(xml, (err, result) => {

                                                // BEGINNING OF "XML2JS.parseString"
                                                console.log('result', result); // This returns data.
                                                game.statistics = result.items.item[0].statistics[0].ratings[0];
                                                // setStatistics(...{statistics}, ...{gameId: result.items.item[0].statistics[0].ratings[0]})
                                               console.log('game', game); // This returns the object with the newly statistics key.
                                               console.log('STATS!', game.statistics); // This doesn't recognize the statistics key?!
        
                                               /* Going through the array and changing default values and converting string numbers to actual numbers */
                                              if (game.stats[0].rating[0].ranks[0].rank[0].$.value === 'Not Ranked')
                    game.stats[0].rating[0].ranks[0].rank[0].$.value = 'N/A';
                                              else {
                                            game.stats[0].rating[0].ranks[0].rank[0].$.value = Number(game.stats[0].rating[0].ranks[0].rank[0].$.value);
                                              }
        
                                           game.stats[0].$.minplayers = Number(game.stats[0].$.minplayers);
                                              if (isNaN(game.stats[0].$.minplayers))
                                            game.stats[0].$.minplayers = '--';
        
                                        game.stats[0].$.maxplayers = Number(game.stats[0].$.maxplayers);
                                        if (isNaN(game.stats[0].$.maxplayers))
                                            game.stats[0].$.maxplayers = '--';
        
                                        game.stats[0].$.maxplaytime = Number(game.stats[0].$.maxplaytime);
                                        if (isNaN(game.stats[0].$.maxplaytime))
                                            game.stats[0].$.maxplaytime = '--';
        
                                        if (game.yearpublished === undefined)
                                            game.yearpublished = ['--'];
                                    });
                                    setGameList(game); // The forEach means that result.items.item == game
                                    // END OF "XML2JS.parseString" 

                                            })
                                        })
    
                            }
                        });
                    } else if (response.status === 202) { // If the status response was 202 (API still retrieving data), call the fetch again after a set timeout
                        setTimeoutAsCallback(() => recursiveFetchAndWait(url));
                    } else
                        console.log(response.status);
                })
        },
        [],
    ); 
3
On

If you want to create a sequencial flow, you should follow as below:

await Promise.all(games.map(async game => {
    await new Promise((resolve) => {
        fetch('https://www.boardgamegeek.com/xmlapi2/thing?id=' + game.$.objectid + '&stats=1')
            .then(async response => {
                const xml = await response.text(); // XML2JS boilerplate

                xml2js.parseString(xml, (err, result) => { // XML2JS boilerplate
                    console.log('result', result); // This returns data.
                    
                    game.statistics = result.items.item[0].statistics[0].ratings[0]; // Creating a new statistics key on the game object and assigning it the statistics from the API call

                    resolve();
                });
            });
        });
    }));


    games.forEach(game => {
      console.log('game', game);
      console.log('STATS!', game.statistics); 
    });
})();
2
On

Move console.log('STATS!', game.statistics); immediately below game.statistics =.

Or, do everything inside an async function:

(async () => {
  for (const game of games) {
    const response = fetch('https://www.boardgamegeek.com/xmlapi2/thing?id=' + game.$.objectid + '&stats=1');
    const xml = await response.text();
    await new Promise(resolve => {
      XML2JS.parseString(xml, (err, result) => {
        game.statistics = result.items.item[0].statistics[0].ratings[0];
        resolve();
      });
    });
  }


  games.forEach(game => {
    console.log('game', game);
    console.log('STATS!', game.statistics); 
  });
})();