Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dispatch an action in response to cancellation

I started with the cancellation recipe from the redux-observable docs and want to extend it a bit.

Basically I have a scenario where after the cancellation is triggered, using takeUntil I want dispatch another action to cleanup, etc.

This is what I came up with so far: https://jsbin.com/zemenu/195/edit?js,output

Start a "Fetch User Info" and then hit Cancel. I want it to execute actions in this order:
- USER/FETCH
- REQUEST/STARTED
- USER/CANCELLED
- REQUEST/CANCELLED

This works in the way I have it setup right now. But, I have to rely on passing dispatch into the requestSequence function and then trigger it in finally. Is there a cleaner way to do this just with observable operators? So when that USER.CANCELLED is triggered some final action is mapped to inside the requestSequence observable.

Redux logger is enabled so check the console for all the actions.

like image 364
winkerVSbecks Avatar asked Mar 11 '23 11:03

winkerVSbecks


1 Answers

Instead of using .takeUntil(), it sounds like you want to use .race(), which is fairly aptly named. Whichever stream emits first, wins! The other is unsubscribed.

You'll need to restructure some things a bit to use it as you want. You want to isolate the first action you emit immediately, your request.onStart(meta), separate from the ajax request Observable.fromPromise(apiCall(...args)). Then you want to race directly between that ajax and the cancellation, so you'd need to pass in the action$ ActionsObservable since you have all this in a helper.

https://jsbin.com/suvaka/edit?js,output

function requestSequence(apiCall, args, meta, action$) {
  return Observable.of(request.onStart(meta))
    .concat(
      Observable.fromPromise(apiCall(...args))
        .map((payload) => request.onSuccess(payload, meta))
        .catch((e) => Observable.of(request.onError(e, meta)))
        .race(
          action$.ofType(USER.CANCELLED)
            .map(() => request.onCancel(meta))
        )
    );
}

const fetchUserEpic = (action$, store) =>
  action$.ofType(USER.FETCH)
    .mergeMap(action =>
      requestSequence(
        userRequest, 
        [`/api/users/${action.payload}`],
        { activity: USER.FETCH, path: 'user' },
        action$
      )   
    );

Side note: be careful about premature abstractions like making those sorts of helpers. Even though you may repeat things in some epics, I've found abstracting it can make it much harder to grok later, especially if it's someone else who didn't write the code and aren't an Rx guru. Only you can know whether this advice applies to you and your codebase, of course.

The primary confusing point for me is all the arguments you have to pass to requestSequence, which will be tough for many to understand when they first come across it. If you find that very very commonly your epics do exactly the same thing and you want to reuse, perhaps abstracting the entire epic would be more clear, and create API utilities like userRequest below that you can test independently.

(untested, basically pseudo code)

const createApiEpic = options =>
  action$ =>
    action$.ofType(options.on)
      .mergeMap(action =>
        Observable.of(request.onStart(meta))
          .concat(
            options.effect(action.payload)
              .map(payload => request.onSuccess(payload, meta))
              .catch(e => Observable.of(request.onError(e, meta)))
              .race(
                action$.ofType(options.cancel)
                  .map(() => request.onCancel(meta))
              )
          )
      );

const userRequest = id =>
  Observable.ajax.getJSON(`/api/users/${id}`);

const fetchUserEpic = createApiEpic({
  on: USER.FETCH,
  effect: userRequest
  cancel: USER.CANCELLED
});
like image 133
jayphelps Avatar answered Mar 19 '23 23:03

jayphelps