Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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
    })
  }
}
like image 417
Stefan R Avatar asked May 05 '20 07:05

Stefan R


2 Answers

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.

like image 136
Andrew Dibble Avatar answered Oct 16 '22 17:10

Andrew Dibble


I guess you could refactor it like the following:

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 ) {
        return Promise.all( things.reduce( (promises, thing) => {
          if (thing.channels) {
            thing.items = [];
            promises.push( ...thing.channels.map( channel => 
              channel.linkedItems && 
              channel.linkedItems.map( item => 
                loadItems(item).then( result => thing.items.push( result ) ) 
              ) ).flat().filter( i => !!i ) );
          }
          return promises;
        }, []) );
      }
      return things;
    }).catch(error => {
      //todo: handle error
      return false
    })
  }
}

In case you would have things, it would check for the channels and the linkedItems for that channel, and create a promise that will push the result back to the thing.items array.

By returning the Promise.all, the continuation of the loadThings would only complete when the Promise.all was resolved. In case there are no things, just things gets returned (which would be a falsy value, so I am wondering how valid that statement could be)

I haven't actually tested the refactoring so there might be some brackets in need of adjusting to your situation, but I guess it gives you an idea?

like image 27
Icepickle Avatar answered Oct 16 '22 19:10

Icepickle