lwisi
lwisi

Reputation: 303

Passing params from one Axios request to another

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);
};

Upvotes: 1

Views: 716

Answers (1)

HMR
HMR

Reputation: 39270

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/[email protected]/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.

Upvotes: 1

Related Questions