0

This program is written using ReactJS and REST API is used to get the data. The problem I am facing is that I am trying to traverse a tree of data and I need to make an async request for each node in the tree.

The tree looks something like this: example tree

Currently, my code traverses the tree in breadth-first order, and the order looks like: 1->2->3->4->4. I need it to traverse the tree in depth-first order, so that the order is 1->3->4->2->4. The goal is to make an API request for each node in depth-first order and push the results into an array.

The data we start off with is an array, [node1, node2], and the result array should be [node1, node3, node4, node2, node4].

The issue is that the async nature of the method allows the code to move to node 2 before we've even traversed and pushed the child data for node 1 into our resulting array. Before we've completely traversed all possible children for node1, it's already moving on to node2 and pushing that into our result array, and then going on to process it as well. Here is an example of what the code looks like currently:

var resultArray = [];

    getNodes(nodesArray) {

        nodesArray.forEach(node => {

          //Store nodes
          resultArray.push(node)

            //make a new request
            const newRequest = node.id;

            //our async request
            getNodeData(newRequest)

                .then((data) => {
                  var childNodes = data.nodes;

                  //Call method again for child nodes
                  this.getNodes(childNodes);
                    

                })
                .catch((error) => {
                    console.log(error)
                })
        })
    }

I have tried using Promises to await for the data to come in, but no luck in getting the .forEach loop to actually wait before moving onto the next node.

Tom
  • 3
  • 1
  • check this question https://stackoverflow.com/questions/37576685/using-async-await-with-a-foreach-loop – 36ve Apr 09 '21 at 20:03

3 Answers3

0

You can make the function async, then use await with a for ... of loop.

async getNodes(nodesArray) {
    for (const node of nodesArray) {
        resultArray.push(node)
        const newRequest = node.id;
        try {
            const res = await getNodeData(newRequest);
            const childNodes = res.nodes;
            await this.getNodes(childNodes);
        } catch (error) {
            console.log(error);
        }
    })
}
Unmitigated
  • 76,500
  • 11
  • 62
  • 80
0

Since getNodes makes asynchronous calls, its result will be a Promise, so you can mark the getNodes method as async.

IMO getNodes should return the list of nodes rather than modifying some external array as a side-effect, so getNodes can be modified to return a Promise that resolves to the list of nodes.

With a for-loop in an async function, you can take advantage of await for both your getNodeData async function call, and the getNodes recursive call.

// returns a Promise that resolves to a list of nodes
async getNodes(nodesArray) {
  const resultArray = [];
  for (const node of nodesArray) {
    // Store nodes
    resultArray.push(node);

    // make a new request
    const newRequest = node.id;

    //our async request
    try {
      const data = await getNodeData(newRequest);
 
      // call method again for child nodes
      resultsArray.concat(await this.getNodes(childNodes)); 
    } catch (error) {
      console.error(error);
    }    
  }
  return resultsArray;
}
Anson Miu
  • 1,171
  • 7
  • 6
0

Promise.all will help here, turning an array of Promises into a Promise for an array of values. We can use this together with a recursive call for the children, combining the parent and children into a single array, and then flattening the result. Something like this should do it:

const getNodes = (nodes) =>
  Promise .all (
    nodes .map (node => getNodeData (node) 
      .then (parent => getNodes (parent.nodes) .then (children => [parent, ...children]))
    )
  ) .then (nodes => nodes .flat())


getNodes ([1, 2]) 
  .then (console .log)
  .catch (err =>  console .log (`Error: ${err}`))
.as-console-wrapper {max-height: 100% !important; top: 0}
<script> /* dummy implementation of getNodeData */ 
const getNodeData = ((data = {1: {id: 1, x: 'foo', nodes: [3, 4]}, 2: {id: 2, x: 'bar', nodes: [4]}, 3: {id: 3, x: 'baz', nodes: []}, 4: {id: 4, x: 'qux', nodes: []}}
) => (id) => new Promise ((res, rej) => 
  setTimeout(() => data[id] ? res(JSON.parse(JSON.stringify(data[id]))) : rej(`${id} not found`)), 100)
)()
</script>

getNodeData is a dummy implementation, just meant to return a quick Promise for your data by id.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103