tradebel123 tradebel123 - 9 days ago 6
Javascript Question

What is the expected behaviour Cancelling async request in Redux-observable using takeUntil

i am new to RXJS, i found Redux-observable canceling async request using takeUntil is very useful. but while i was testing it i found that the actual request is still going on even though we cancel the request..

i have this JSbin code snippet to test.

https://jsbin.com/hujosafocu/1/edit?html,js,output

here the actual request is not canceling, even if you cancel the request by clicking the cancel (multiple times) button.

i am not sure this is how it should be.. if yes, then what does it meant by canceling async request. I am bit confused.. Please share some thoughts..

any respond to this will greatly appreciate.. thanks

Answer

The issue is very subtle, but obviously important. Given your code:

const fetchUserEpic = action$ =>
  action$.ofType(FETCH_USER)
    .delay(2000) // <-- while we're waiting, there is nothing to cancel!
    .mergeMap(action =>
      Observable.fromPromise(
        jQuery.getJSON('//api.github.com/users/redux-observable', data => {
          alert(JSON.stringify(data));
        })
      )
      .map(fetchUserFulfilled)
      .takeUntil(action$.ofType(FETCH_USER_CANCELLED))
    );

The kicker is the .delay(2000). What this is saying is, "don't emit the action to the rest of the chain until after 2000ms". Because your .takeUntil(action$.ofType(FETCH_USER_CANCELLED)) cancellation logic is inside the mergeMap's projection function, it is not yet listening for FETCH_USER_CANCELLED because there is nothing to cancel yet!

If you really want to introduce an arbitrary delay before you make the ajax call, but cancel both the delay OR the pending ajax (if it reaches there) you can use Observable.timer()

const fetchUserEpic = action$ =>
  action$.ofType(FETCH_USER)
    .mergeMap(action =>
      Observable.timer(2000)
        .mergeMap(() =>
          Observable.fromPromise(
            jQuery.getJSON('//api.github.com/users/redux-observable', data => {
              alert(JSON.stringify(data));
            })
          )
          .map(fetchUserFulfilled)
        )
        .takeUntil(action$.ofType(FETCH_USER_CANCELLED))
    );

I imagine you don't really want to introduce the arbitrary delay before your ajax calls in real-world apps, in which case this problem won't exist and the example in the docs is a good starting reference.


Another thing to note is that even without the delay or timer, cancelling the ajax request from your code doesn't cancel the real underlying XMLHttpRequest--it just ignores the response. This is because Promises are not cancellable.

Instead, I would highly recommend using RxJS's AjaxObservable, which is cancellable:

Observable.ajax.getJSON('//api.github.com/users/redux-observable')

This can be imported in several ways. If you're already importing all of RxJS a la import 'rxjs';, it's available as expected. Otherwise, there are several other ways:

import { ajax } from 'rxjs/observable/dom/ajax';

ajax.getJSON('/path/to/thing');

// or

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/dom/ajax';

Observable.ajax.getJSON('/path/to/thing');

It's important to remember, like all the Observable factories, Observable.ajax is lazy meaning it does not make the AJAX request until someone subscribes to it! Where as jQuery.getJSON makes it right away.

So you can put it together like this:

const fetchUserEpic = action$ =>
  action$.ofType(FETCH_USER)
    .mergeMap(action =>
      Observable.timer(2000)
        .mergeMap(() =>
          Observable.ajax.getJSON('//api.github.com/users/redux-observable')
            .do(data => alert(JSON.stringify(data)))
            .map(fetchUserFulfilled)
        )
        .takeUntil(action$.ofType(FETCH_USER_CANCELLED))
    );

A working demo of this can be found here: https://jsbin.com/podoke/edit?js,output

Comments