Search code examples
reduxreact-reduxredux-promise

React-redux project - chained dependent async calls not working with redux-promise middleware?


I'm new to using redux, and I'm trying to set up redux-promise as middleware. I have this case I can't seem to get to work (things work for me when I'm just trying to do one async call without chaining)

Say I have two API calls:

1) getItem(someId) -> {attr1: something, attr2: something, tagIds: [...]}
2) getTags() -> [{someTagObject1}, {someTagObject2}]

I need to call the first one, and get an item, then get all the tags, and then return an object that contains both the item and the tags relating to that item.

Right now, my action creator is like this:

export function fetchTagsForItem(id = null, params = new Map()) {
    return {
        type: FETCH_ITEM_INFO,
        payload: getItem(...) // some axios call
            .then(item => getTags() // gets all tags 
                .then(tags => toItemDetails(tags.data, item.data)))
    }
}

I have a console.log in toItemDetails, and I can see that when the calls are completed, we eventually get into toItemDetails and result in the right information. However, it looks like we're getting to the reducer before the calls are completed, and I'm just getting an undefined payload from the reducer (and it doesn't try again). The reducer is just trying to return action.payload for this case.

I know the chained calls aren't great, but I'd at least like to see it working. Is this something that can be done with just redux-promise? If not, any examples of how to get this functioning would be greatly appreciated!


Solution

  • I filled in your missing code with placeholder functions and it worked for me - my payload ended up containing a promise which resolved to the return value of toItemDetails. So maybe it's something in the code you haven't included here.

    function getItem(id) {
      return Promise.resolve({
        attr1: 'hello',
        data: 'data inside item',
        tagIds: [1, 3, 5]
      });
    }
    
    function getTags(tagIds) {
      return Promise.resolve({ data: 'abc' });
    }
    
    function toItemDetails(tagData, itemData) {
      return { itemDetails: { tagData, itemData } };
    }
    
    function fetchTagsForItem(id = null) {
      let itemFromAxios;
      return {
        type: 'FETCH_ITEM_INFO',
        payload: getItem(id)
          .then(item => {
            itemFromAxios = item;
            return getTags(item.tagIds);
          })
          .then(tags => toItemDetails(tags.data, itemFromAxios.data))
      };
    }
    
    
    const action = fetchTagsForItem(1);
    action.payload.then(result => {
      console.log(`result:  ${JSON.stringify(result)}`);
    });
    

    Output:

    result:  {"itemDetails":{"tagData":"abc","itemData":"data inside item"}}
    

    In order to access item in the second step, you'll need to store it in a variable that is declared in the function scope of fetchTagsForItem, because the two .thens are essentially siblings: both can access the enclosing scope, but the second call to .then won't have access to vars declared in the first one.

    Separation of concerns

    The code that creates the action you send to Redux is also making multiple Axios calls and massaging the returned data. This makes it more complicated to read and understand, and will make it harder to do things like handle errors in your Axios calls. I suggest splitting things up. One option:

    • Put any code that calls Axios in its own function
    • Set payload to the return value of that function.
    • Move that function, and all other funcs that call Axios, into a separate file (or set of files). That file becomes your API client.

    This would look something like:

    // apiclient.js
    const BASE_URL = 'https://yourapiserver.com/';
    const makeUrl = (relativeUrl) => BASE_URL + relativeUrl; 
    function getItemById(id) {
      return axios.get(makeUrl(GET_ITEM_URL) + id);
    }
    
    function fetchTagsForItemWithId(id) {
    ...
    }
    
    // Other client calls and helper funcs here
    
    export default {
      fetchTagsForItemWithId
    };
    

    Your actions file:

    // items-actions.js
    import ApiClient from './api-client';
    function fetchItemTags(id) {
      const itemInfoPromise = ApiClient.fetchTagsForItemWithId(id);
      return {
        type: 'FETCH_ITEM_INFO',
        payload: itemInfoPromise 
      };
    }