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.
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
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With