jeffbski / redux-logic

Redux middleware for organizing all your business logic. Intercept actions and perform async processing.
MIT License
1.81k stars 107 forks source link

Feature Request: catchMiddleware request #45

Open kopax opened 7 years ago

kopax commented 7 years ago

I need to dispatch a logout event when I receive a err.statusCode = 401.

So far, I have two solutions available:

  1. write this on each logic: inconvenient
  2. Pass the dispatch to my fetch wrapper function, then dispatch the action from: not the place to do it and also inconveniant

You recommended us in the documentation to use Observable to cancel an ajax call. Since fetch doesn't have a way to cancel an ajax request, and also can't be configured globally, I think it will be a nice feature if you add the possibility to catch globally the logic errors.

erkiesken commented 7 years ago

How I did it is wrap all external REST API fetch calls in an RestApi class singleton, that is then passed as dependency to logics. So in typical logic I use it like so:

process({ action, api }, dispatch, done) {
  ...
  api.getSomething()
    .then(() => { successful result dispatch(...) })
    .catch(() => { some logging or other error action dispatch() })
    .then(() => done());
}

So api call specific error handling (validation errors or whatnot) goes here as normal.

And in the RestApi class I wrap fetch calls something like so:

class RestApi {
  constructor() {
    this.errors$ = new Rx.Subject(); // all fetch errors get written to this stream in addition to being returned as promise rejections
  },
  _fetchRequest(url, opts) {
    let status;
    return fetch(url, opts)
      .then(res => {
          status = res.status;
          if (status >= 400) {
               return Promise.reject(...i actually map stuff to specific custom error classes here)
          }
          return res.json();
      })
      .catch(err => {
          this.errors$.next(err); // here all errors, including the 401 are passed
          return Promise.reject(err);
      });
  },
  getSomething(..) {
    // prep req params and call
    return this._fetchRequest(...);
}}

const api = new RestApi();
export default api;

And now somewhere where store and dispatch are available, like your bootstrap script or main root view component you can listen to the errors$ stream, filter 401s and dispatch a specific app login action:

api
  .errors$
  .filter((err) => err instanceof AuthenticationRequiredError)
  .subscribe((err) => {
    log("Auth needed, redirecting to login.", err.message);
    dispatch(loginRequired());
  });

This way any REST API call anywhere in logics will trigger loginRequired on 401 without having to sprinkle this special error handling all over the place.

HTH.

kopax commented 7 years ago

Hi @tehnomaag, I was waiting for @jeffbksi feedback on this but now I see he is not close to this repo anymore.

What is in your code new Rx.Subject(); ?

Could you elaborate a bit your example ? I am still facing the same issue.

erkiesken commented 7 years ago

What is in your code new Rx.Subject(); ?

That's just an observable stream from RxJS project. RxJS is also what redux-logic is built upon internally. To get the minimum amount of dependencies import Subject directly, not the whole Rx namespace, ie:

import { Subject } from "rxjs/Subject";

jeffbski commented 7 years ago

@kopax Sorry for the delay. I've been thinking through the next round of changes and how to accomplish. It's a balancing act of when to add new features and how to add them so that they don't cause confusion and complication.

Thanks for mentioning @tehnomaag yes, rxjs is a great way to enable this sort of thing.

They are working on an official mechanism for cancellation with fetch, so that is coming. In the meantime rxjs ajax supports this built-in and you can also do similar with axios with a little more wiring. As @tehnomaag mentioned it can also just be handled in your api functions.