3

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 Answers 4

2

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Actually I am already doing that in my UserActions, but I am calling a decoupled api.service from that action, which I might use as well for requests not related to a redux-action. But what I did now is to always pass dispatch from the action to the service. Thanks!
0

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

Comments

0

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(...)
  }
}

2 Comments

Thanks for your reply, but in your example I still have to import the store and call store.dispatch(UserActions.logout()); ( which is exactly what I am doing right now). As described in the link I provided, this is bad because the store needs to be a singleton. Or did I miss something?
Ah I see what you mean. Actually I dont need to store.dispatch, just dispatch. But that would mean I have to handle rejects in EVERY api action, and it would be way cleaner to handle it all in my api.services.ts
0

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.