Search code examples
javascriptreactjspromisesynchronous

Synchronously populate/modify an array and return the modified array inside a promise


I'm pretty new to ReactJS and redux, so I've never really had to work with this before. I'm retrieving data from an API call in my project. I want to modify the data by adding a new property to the object. However, because the code is not ran synchronously, the unmodified array is being returned (I assume) instead of the modified array.

export function loadThings() {
  return dispatch => {
    return dispatch({
     type: 'LOAD_THINGS',
     payload: {
       request: {
         url: API_GET_THINGS_ENDPOINT,
         method: 'GET'
       }
     }
    }).then(response => {
      let things = response.payload.data;
      // Retrieve all items for the loaded "things"
      if(things) {
        things.forEach((thing, thingIndex) => {
            things[thingIndex].items = []
            if (thing.hasOwnProperty('channels') && thing.channels) {
              thing.channels.forEach(channel => {
                if (channel.hasOwnProperty('linkedItems') && channel.linkedItems) {
                  channel.linkedItems.forEach(itemName => {
                    dispatch(loadItems(itemName)).then(
                      item => things[thingIndex].items.push(item) // push the items inside the "things"
                    )
                  })
                }
              })
            }
        })
      }
      things.forEach(data => console.log(data.items.length, data.items)) // data.items.length returns 0, data.items returns a populated array
      return things // return the modified array
    }).catch(error => {
      //todo: handle error
      return false
    })
  }
}

As you can see, I perform an API call which returns data named response. The array is populated with all "things". If things exists, I want to load extra information named "items". Based on the information in the things array, I will perform another API call (which is done by dispatching the loadItems function) which returns another promise. Based on the data in the results of that API call, I will push into the items property (which is an array) of the things object.

As you can see in the comments, if I loop through the things array and log the items property which I just created, it's basically returning 0 as length, which means the things array is being returned before the things array is being modified.

I would like to know two things:

  • What is causing my code to run async. Is it the dispatch(loadItems(itemName)) function since it returns a promise?
  • How am I able to synchronously execute my code?

Please note: this function loadThings() also returns a promise (if you're not familair with redux).

You might be interested in knowing what I tried myself to fix the code

Since I fail to understand the logic why the code is ran async, I've been trying hopeless stuff. Such as wrapping the code in another Promise.all and return the modified array in that promise. I used the then method of that promise to modify the things array, which had the exact same result. Probably because return things is being executed outside of that promise.

I'd really love to know what is going on

Edit I have added the loadItems() code to the question, as requested:

export function loadItems(itemName) {
  return dispatch => {
    const url = itemName ? API_GET_ITEMS_ENDPOINT + `/${itemName}` : API_GET_ITEMS_ENDPOINT;
    return dispatch({
      type: 'LOAD_ITEMS',
      payload: {
        request: {
          url: url,
          method: 'GET'
        }
      }
    }).then(response => {
      return response.payload.data
    })
  }
}

Solution

  • My approach would be to map over things, creating arrays of promises for all of their items wrapped in a Promise.all, which gives you an array of Promise.all's.

    Then you return things and this array of promises in another Promise.all and in the next then block, you can just assign the arrays to each thing with a simple for loop:

    export function loadThings() {
      return dispatch => {
        return dispatch({
          type: 'LOAD_THINGS',
          payload: {
            request: {
              url: API_GET_THINGS_ENDPOINT,
              method: 'GET'
            }
          }
        }).then(response => {
          let things = response.payload.data;
          // Retrieve all items for the loaded "things"
          const items = things.map((thing) => {
            const thingItems = []
            if (thing.hasOwnProperty('channels') && thing.channels) {
              thing.channels.forEach(channel => {
                if (channel.hasOwnProperty('linkedItems') && channel.linkedItems) {
                  channel.linkedItems.forEach(itemName => {
                    thingItems.push(dispatch(loadItems(itemName)));
                  });
                }
              });
            }
    
            return Promise.all(thingItems);
          });
    
          return Promise.all([things, Promise.all(items)])
        })
        .then(([things, thingItems]) => {
          things.forEach((thing, index) => {
            thing.items = thingItems[index];
          })
    
          return things;
        })
        .catch(error => {
          //todo: handle error
          return false
        })
      }
    }
    

    Edit: You need to push the dispatch(loadItmes(itemName)) calls directly into thingItems.