1

Background

I'm connecting an app built in React Native to a REST API. I'm handling requests via Axios and storing the results from queries with Redux. I have an index.js file for my api connections which holds the functions that act as handlers for requests which require deeper and deeper levels of authorization. I have a simple function which returns an access token, this is triggered by the following code which currenty is located in the app's "Welcome page".

useEffect(() => {
    dispatch(retrieveToken());
  }, [dispatch]);

Ideally, after navigating through a couple of screens, the user would get to the Home Page and trigger the following code:

useEffect(() => {
    dispatch(retrieveData());
  }, [dispatch]);

So far, so good. These are the functions which dispatch triggers:

 export const getToken = () =>
      apiInstance
        .request({
          url: ENDPOINTS.TOKEN,
          data: qs.stringify({
            grant_type: 'some_credentials',
            c_id: 'some_id',
            c_secret: 'some_secret',
          }),
          headers: {
            'content-type': 'some_content_type',
          },
          method: 'POST',
        })
        .then(response => {
          return response.data;
        })
        .catch(error => {
          return Promise.reject(error.message);
        });

export const getData = () =>
  apiInstance
    .request({
      url: ENDPOINTS.DATA,
      method: 'POST',
      data: qs.stringify({
        timestamp: Date.now(),
        c_id: 'some_id',
        token: **this is the token we get from the previous function**,
      }),
      headers: {
        'content-type': 'some_content_type',
      },
    })
    .then(response => {
      return response.data;
    })
    .catch(error => {
      return Promise.reject(error.message);
    });

Problem

As I mentioned before, this is a Redux/Axios solution. This means state is stored globally but there is an order of execution. You should note that these two functions are stored within the same file and are not called upon unless explicitly stated such as with the two dispatch calls I've showed before.

Thing is, if I log the token from Home (after calling it with dispatch) I can see it clearly, however if I try to log said token from the file which stores the request functions, I get an empty array. I've tried to fill the token field in all the following ways:

  1. const state = store.getState() token: state.token
  2. const getData = (Token) =>{ ... token: Token} And passing Token as a param within dispatch.
  3. I've also tried daisy-chaining the different dispatches in order to force the execution of getData after retrieving the token and not before.

Question

How can I access the result of an axios query from within another, in specific order?

It is very important to note that the data in the API can only be accessed via POST and that the error code I get when executing getData() is 401, incorrect credentials. Also, remember this is a Redux application. The results of the queries are stored withing a global state. However this state cannot be accessed from outside components, and I cannot call it from the file in which the queries are executed given the token "does not exist at that point in time."

Action code

import keyMirror from 'keymirror';
import {createAction} from 'redux-actions';
import {getToken} from '../../api';

export const tokenActionTypes = keyMirror({
  RETRIEVE_TOKEN_REQUEST: null,
  RETRIEVE_TOKEN_SUCCESS: null,
  RETRIEVE_TOKEN_FAILURE: null,
});

const tokenActionCreators = {
  request: createAction(tokenActionTypes.RETRIEVE_TOKEN_REQUEST),
  success: createAction(tokenActionTypes.RETRIEVE_TOKEN_SUCCESS),
  failure: createAction(tokenActionTypes.RETRIEVE_TOKEN_FAILURE),
};

export const retrieveToken = () => dispatch => {
  dispatch(tokenActionCreators.request());
  getToken()
    .then(token => dispatch(tokenActionCreators.success(token)))
    .catch(error => dispatch(tokenActionCreators.failure(error)));
};

Reducer code

import {tokenActionTypes} from '../actions/token';

export const initialState = {
  loadingToken: false,
  token: [],
  error: null,
};

const actionsMap = {
  [tokenActionTypes.RETRIEVE_TOKEN_REQUEST]: state => ({
    ...state,
    loadingToken: true,
  }),

  [tokenActionTypes.RETRIEVE_TOKEN_SUCCESS]: (state, action) => ({
    ...state,
    loadingToken: false,
    token: action.payload,
  }),

  [tokenActionTypes.RETRIEVE_TOKEN_FAILURE]: (state, action) => ({
    ...state,
    loadingToken: false,
    error: action.payload,
  }),
};

export default (state = initialState, action) => {
  const actionHandler = actionsMap[action.type];
  if (!actionHandler) {
    return state;
  }
  return actionHandler(state, action);
};
lwisi
  • 293
  • 8
  • 23
  • `return response.data;` does nothing; `getToken()` returns `undefined`. See here: https://stackoverflow.com/questions/23667086/why-is-my-variable-unaltered-after-i-modify-it-inside-of-a-function-asynchron –  Jun 07 '21 at 13:39
  • Also, dupe: [How to return the response from an asynchronous call?](https://stackoverflow.com/questions/14220321/how-to-return-the-response-from-an-asynchronous-call) –  Jun 07 '21 at 13:40
  • @ChrisG It doesn't return undefined. I can log the token out. – lwisi Jun 07 '21 at 13:50
  • You can log it in the `.then()` function, sure, but not anywhere else. –  Jun 07 '21 at 13:51
  • @ChrisG It is stored globally via redux. I can access it anywhere by using a selector from Redux: const token = useSelector(state => state.token); Thing is, this only works within components. – lwisi Jun 07 '21 at 13:54
  • You could just store it in the script. Put `let token = null;` at the top, then set it inside `getToken` and use it in other requests. –  Jun 07 '21 at 14:03
  • @ChrisG and how would I set it inside getToken? – lwisi Jun 07 '21 at 14:11
  • How are you putting it in the store? Anyway, after `.then(response => {` you would do `token = response.data;` –  Jun 07 '21 at 14:12
  • @ChrisG I'm handling 3 actions and via a reducer storing all response.data in three separate fields: error, loading and payload. Note that response.data is an object. – lwisi Jun 07 '21 at 14:21
  • Whatever ENDPOINTS.TOKEN returns, store it in a "global" variable. Then use that in your other API functions. I'm don't see how the redux store factors into that. –  Jun 07 '21 at 14:25
  • @ChrisG When getToken() is triggered, it saves response.data in the Redux store and global state. I can access that state from any component from there on out. For example, if I call getToken from "screen 1", I could be in "screen 43" and still be able to access it via `token = useSelector(state => state.token)`. I do not understand why I cannot access it from another axios request. If I get state.token in the file which holds the requests and pass it as data for getData(), it isn't able to access it. – lwisi Jun 07 '21 at 14:40
  • Can you show the actual code you use? Your response handlers don't store anything, and there's no param in your `getData()` function. Anyway, passing the token to `getData()` should definitely work. Still wondering how you're getting the token though, given that again: getToken does run the request but the function as it is in your question does not return anything. –  Jun 07 '21 at 14:43
  • I'll add action and reducer code. – lwisi Jun 07 '21 at 14:46
  • @ChrisG I just noticed I had forgotten to show a lot of important code and that I have messed up naming in an attempt to anonimize data. Check everything now. – lwisi Jun 07 '21 at 14:49
  • Sorry, my bad, you're using short arrow syntax so the functions do indeed return the Promises. Still, something like `getData(token)` should solve this quickly. –  Jun 07 '21 at 14:50
  • @ChrisG I've tried that but I'll give it a go just in case I've messed something up. – lwisi Jun 07 '21 at 14:52
  • @ChrisG so in the statement of the function I add a parameter called token, I pass that param as token: token and then within the dispatch call I add getData(token). That doesn't change anything. Am I missing something? – lwisi Jun 07 '21 at 14:55
  • `export const getData = () =>` becomes `export const getData = token =>`. Now you can use that in your request code. Wherever you later call `getData()` you simply do `getData(token)` instead –  Jun 07 '21 at 15:14
  • @ChrisG got it, had to prop drill a whole lot so I'll be looking for a different and more stable solution. Thanks a lot! – lwisi Jun 07 '21 at 15:49
  • @ChrisG The problem with passing token to thunk from component or composed thunk or storing it in a global variable is that during one render cycle it may try to fetch a token multiple times. This can easily be solved by [grouping the thunk](https://gist.github.com/amsterdamharu/2dde4a6f531251f3769206ee44458af7) and wrap thunks that need a token in a `needsToken` function to prevent duplication (example in my answer). – HMR Jun 07 '21 at 18:06

1 Answers1

1

You could combine one thunk in another, like combining get token in get data:

export const retrieveToken = () => (dispatch, getState) => {
  //you could use getState() to see if you need to fetch the token
  // const tokenResult = selectToken(getState());
  // if(token && !token expired) { return Promise.resolve() }
  dispatch(tokenActionCreators.request());
  //return a promise so you can wait for it
  return getToken()
    .then(token => dispatch(tokenActionCreators.success(token)))
    .catch(error => dispatch(tokenActionCreators.failure(error)));
};
//in retrieve data you can wait for the token
export const retrieveData = () => dispatch => {
  dispatch(retrieveToken()).then(
    ()=>{
      //here return getting the data
    }
  )
};

A possible bug in that code is that one render cycle will dispatch multiple thunks that will get the token multiple times. You can solve that by grouping the retrieveToken action with a cache that invalidates on resolve:

const invalidateOnResolveCache = (cache = new Map()) => {
  return {
    get: (key) => cache.get(key),
    set: (key, value) => cache.set(key, value),
    resolved: (x) => cache.delete(key),
  };
};

Or you can write a wrap function for all thunks that need a token:

//group retrieveToken in such a way that if it's called multiple times
//  during a render cycle the token request will only be made once
//https://gist.github.com/amsterdamharu/2dde4a6f531251f3769206ee44458af7
export const needsToken =
  (fn) =>
  (...args) =>
  (dispatch, getState) =>
    dispatch(retrieveToken(...args)).then(() =>
      //you could use getState to get the token and pass it to
      //  fn together with the other args
      // for example: fn(...args.concat(selectToken(getState())))
      fn(...args)
    );
export const autoTokenRetrieveData = needsToken(retrieveData);
//use needsToken for any other thunk actions that need a token

Example:

const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;

//grouping code to group your actions
//group promise returning function
const createGroup =
  (cache) =>
  (fn, getKey = (...x) => JSON.stringify(x)) =>
  (...args) => {
    const key = getKey(args);
    let result = cache.get(key);
    if (result) {
      return result;
    }
    //no cache
    result = Promise.resolve(fn.apply(null, args)).then(
      (r) => {
        cache.resolved(key); //tell cache promise is done
        return r;
      },
      (e) => {
        cache.resolve(key); //tell cache promise is done
        return Promise.reject(e);
      }
    );
    cache.set(key, result);
    return result;
  };
//thunk action creators are not (...args)=>result but
//  (...args)=>(dispatch,getState)=>result
//  so here is how we group thunk actions
const createGroupedThunkAction = (thunkAction, cache) => {
  const group = createGroup(cache)(
    (args, dispatch, getState) =>
      thunkAction.apply(null, args)(dispatch, getState)
  );

  return (...args) =>
    (dispatch, getState) => {
      return group(args, dispatch, getState);
    };
};
const createInvalidateOnResolveCache = (
  cache = new Map()
) => {
  return {
    get: (key) => cache.get(key),
    set: (key, value) => cache.set(key, value),
    resolved: (key) => cache.delete(key),
  };
};
//function that fetches token
const uniqueToken = (
  (token) => () =>
    token++
)(1);
const fetchToken = () => Promise.resolve(uniqueToken());

const initialState = {
  data1: [],
  data2: [],
  token: null,
};
//action types
const DATA_SUCCESS = 'DATA_SUCCESS';
const GOT_TOKEN = 'GOT_TOKEN';
//action creators
const dataSuccess = (data, key) => ({
  type: DATA_SUCCESS,
  payload: { key, data },
});
const gotToken = (token) => ({
  type: GOT_TOKEN,
  payload: token,
});
const reducer = (state, { type, payload }) => {
  if (type === DATA_SUCCESS) {
    const { data, key } = payload;
    return {
      ...state,
      [key]: data,
    };
  }
  if (type === GOT_TOKEN) {
    return {
      ...state,
      token: {
        value: payload,
        created: Date.now(),
      },
    };
  }
  return state;
};
//thunk getting the data
const getData1 = (token) => (dispatch) =>
  Promise.resolve().then(() =>
    dispatch(
      dataSuccess(
        `got data 1 with token: ${token}`,
        'data1'
      )
    )
  );
const getData2 = (token) => (dispatch) =>
  Promise.resolve().then(() =>
    dispatch(
      dataSuccess(
        `got data 2 with token: ${token}`,
        'data2'
      )
    )
  );
//thunk getting the token:
const getToken = () => (dispatch) =>
  fetchToken().then((token) => dispatch(gotToken(token)));
//grouped thunk getting token
const getTokenGrouped = createGroupedThunkAction(
  getToken,
  createInvalidateOnResolveCache()
);
const needsToken =
  (fn) =>
  (...args) =>
  (dispatch, getState) => {
    let promise;
    //only fetch token if it's older than 1 second
    const tokenResult = selectToken(getState());
    if (
      tokenResult &&
      Date.now() - new Date(tokenResult.created).getTime() <
        1000
    ) {
      promise = Promise.resolve();
    } else {
      promise = dispatch(getTokenGrouped(...args));
    }
    return promise.then(() =>
      dispatch(
        fn(...args.concat(selectTokenValue(getState())))
      )
    );
  };
const getData1WithToken = needsToken(getData1);
const getData2WithToken = needsToken(getData2);
//selectors
const selectData1 = (state) => state.data1;
const selectData2 = (state) => state.data2;
const selectToken = (state) => state.token;
const selectTokenValue = createSelector(
  [selectToken],
  //SO snippet has no optional chaining, should just return token?.value
  (token) => token && token.value
);
//creating store with redux dev tools
const composeEnhancers =
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
  reducer,
  initialState,
  composeEnhancers(
    applyMiddleware(
      //simple thunk middleware
      ({ dispatch, getState }) =>
        (next) =>
        (action) =>
          typeof action === 'function'
            ? action(dispatch, getState)
            : next(action)
    )
  )
);
const Data1 = React.memo(function Data1({ refresh }) {
  const data = useSelector(selectData1);
  const dispatch = useDispatch();
  React.useEffect(() => {
    dispatch(getData1WithToken());
  }, [dispatch, refresh]);
  return <div>{data}</div>;
});
const Data2 = React.memo(function Data2({ refresh }) {
  const data = useSelector(selectData2);
  const dispatch = useDispatch();
  React.useEffect(() => {
    dispatch(getData2WithToken());
  }, [dispatch, refresh]);
  return <div>{data}</div>;
});
const App = () => {
  const [refresh, setRefresh] = React.useState({});
  return (
    <div>
      {/* getting data in one render cycle many times */}
      <Data1 refresh={refresh} />
      <Data2 refresh={refresh} />
      <Data1 refresh={refresh} />
      <Data2 refresh={refresh} />
      <Data1 refresh={refresh} />
      <Data2 refresh={refresh} />
      <button onClick={() => setRefresh({})}>
        refresh
      </button>
    </div>
  );
};

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<script src="https://unpkg.com/immer@7.0.5/dist/immer.umd.production.min.js"></script>
<div id="root"></div>

Explanation:

Everywhere you see const add export so export const or export default and you can import that from any other file.

The createGroupedThunkAction receives getToken thunk and returns a thunk that is stored in getTokenGrouped.

When getTokenGrouped is called multiple times during one render (Data1 and Data2 have an effect that will do so) it will share getting the token for that render and when it resolves it'll delete the cache because of the type of cache used implemented in createInvalidateOnResolveCache. So no multiple tokens will be fetched during one render no matter how many times you dispatch it during a render.

The needsToken function will receive a thunk (getData1 and getData2) and returns a thunk that will automatically get a token by dispatching getTokenGrouped if there is no current token or if the token is older than one second (my made up logic to invalidate the token). This token is stored in state and passed to getData1 and getData2 so they can use that token.

I suggest opening the redux devtools while running the example so you can see the actions being dispatched. Normally with async you would dispatch beforeFetch, afterFetch or faildFetch for async actions but for simplicity I left that out.

You could try to use createGroupedThunkAction to make a grouped getData1 and getData2 as an exercise so there is no needless fetching for these as well.

HMR
  • 37,593
  • 24
  • 91
  • 160
  • In my project I've divided my code in separate files and folders. api/index.js holds the getX() functions, then store/actions/token holds the action code and reducers/token holds the reducer code. Also screens/Home/home.js is where retrieveData is dispatched. I do not understand where I'm meant to apply what you've shown me. I'm new to this and your response has been a bit overwhelming. Could you please explain each part with more detail? – lwisi Jun 07 '21 at 16:49
  • 1
    This answer is absolutely amazing. Thanks a whole lot. – lwisi Jun 07 '21 at 19:37