0

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!

CustardBun
  • 3,457
  • 8
  • 39
  • 65
  • Why would you say chained calls aren't great? They're absolutely necessary when you have one async behavior that depends on the result of a previous async behavior. – stone Jan 21 '18 at 03:09
  • Can you post the version that works, with a single async call? – stone Jan 21 '18 at 03:13

1 Answers1

0

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 
  };
}
stone
  • 8,422
  • 5
  • 54
  • 66
  • Does it still work if you replace the second argument for toItemDetails with item.data? I believe I was having trouble getting that to work without nesting. ( I made a typo in my question - fixed now) – CustardBun Jan 21 '18 at 03:56
  • Nesting results in `item` being declared in the scope that encloses the second call to `then()`. To make it work without the nesting, store `item` in a variable that is declared in the scope that encloses both calls to `then()`: the `fetchTagsForItem` function scope. See updated code. – stone Jan 21 '18 at 05:53