3
votes

I am building an react / redux webapp where I am using a service to make all my API calls. Whenever the API returns 401 - Unauthorized I want to dispatch a logout action to my redux store.

The problem is now that my api-service is no react component, so I cannot get a reference to dispatch or actions. What I did first was exporting the store and calling dispatch manually, but as I read here How to dispatch a Redux action with a timeout? that seems to be a bad practice because it requires the store to be a singleton, which makes testing hard and rendering on the server impossible because we need different stores for each user.

I am already using react-thunk (https://github.com/gaearon/redux-thunk) but I dont see how I can injectdispatch` into non-react components.

What do I need to do? Or is it generally a bad practice to dispatch actions outside from react components? This is what my api.services.ts looks like right now:

... other imports
// !!!!!-> I want to get rid of this import
import {store} from '../';

export const fetchWithAuth = (url: string, method: TMethod = 'GET', data: any = null): Promise<TResponseData> => {
  let promise = new Promise((resolve, reject) => {
    const headers = {
      "Content-Type": "application/json",
      "Authorization": getFromStorage('auth_token')
    };
    const options = {
      body: data ? JSON.stringify(data) : null,
      method,
      headers
    };
    fetch(url, options).then((response) => {
      const statusAsString = response.status.toString();
      if (statusAsString.substr(0, 1) !== '2') {
        if (statusAsString === '401') {
          //  !!!!!-> here I need to dispatch the logout action
          store.dispatch(UserActions.logout());
        }
        reject();
      } else {
        saveToStorage('auth_token', response.headers.get('X-TOKEN'));
        resolve({
          data: response.body,
          headers: response.headers
        });
      }
    })
  });
  return promise;
};

Thanks!

4

4 Answers

2
votes

If you are using redux-thunk, you can return a function from an action creator, which has dispatch has argument:

const doSomeStuff = dispatch => {
  fetch(…)
   .then(res => res.json())
   .then(json => dispatch({
     type: 'dostuffsuccess',
     payload: { json }
    }))
    .catch(err => dispatch({
      type: 'dostufferr',
      payload: { err }
     }))
}

Another option is to use middleware for remote stuff. This works the way, that middle can test the type of an action and then transform it into on or multiple others. have a look here, it is similar, even if is basically about animations, the answer ends with some explanation about how to use middleware for remote requests.

0
votes

maybe you can try to use middleware to catch the error and dispatch the logout action, but in that case, the problem is you have to dispatch error in action creator which need to check the log status

api: throw the error

        if (statusAsString === '401') {
          //  !!!!!-> here I need to dispatch the logout action
          throw new Error('401')
        }

action creator: catch error from api, and dispatch error action

    fetchSometing(ur)
      .then(...)
      .catch(err => dispatch({
        type: fetchSometingError,
        err: err 
       })

middleware: catch the error with 401 message, and dispatch logout action

const authMiddleware = (store) => (next) => (action) => {
  if (action.error.message === '401') {
    store.dispatch(UserActions.logout())
  }
}
0
votes

You should have your api call be completely independent from redux. It should return a promise (like it currently does), resolve in the happy case and reject with a parameter that tells the status. Something like

if (statusAsString === '401') {
  reject({ logout: true })
}
reject({ logout: false });

Then in your action creator code you would do:

function fetchWithAuthAction(url, method, data) {

  return function (dispatch) {
    return fetchWithAuth(url, method, data).then(
      ({ data, headers }) => dispatch(fetchedData(data, headers)),
      ({ logout }) => {
        if(logout) {
          dispatch(UserActions.logout());
        } else {
          dispatch(fetchedDataFailed());
        }
    );
  };
}

Edit:

If you don't want to write the error handling code everywhere, you could create a helper:

function logoutOnError(promise, dispatch) {
  return promise.catch(({ logout }) => {
    if(logout) {
      dispatch(UserActions.logout());
    }
  })
}

Then you could just use it in your action creators:

function fetchUsers() {
  return function (dispatch) {
    return logoutOnError(fetchWithAuth("/users", "GET"), dispatch).then(...)
  }
}
0
votes

You can also use axios (interceptors) or apisauce (monitors) and intercept all calls before they goes to their handlers and at that point use the

// this conditional depends on how the interceptor works on each api.
// In apisauce you use response.status

if (response.status === '401') {
    store.dispatch(UserActions.logout())
}