Debouncing and cancelling with redux-observable

Viewed 4844

I am trying to create a simple redux-observable epic which debounces and is cancelable. My code:

export const apiValidate = action$ => {
    return action$.ofType(validateRequestAction)
        .debounceTime(250)
        .switchMap((action) => (
            Observable.ajax({
                url: url,
                method: 'GET',
                crossDomain: true,
                headers: {
                    "Content-Type": 'application/json'
                },
                responseType: 'json'
            })
           .map(payload => (new APISuccessValidate()))
           .takeUntil(action$.ofType(validateCancelAction))
           .catch(payload => ([new APIFailureValidate()]))
    ));
};

The code only works sometimes. Depending on the speed of the response from the server, I believe 1 of 2 scenarios can occur.

Scenario 1 (works):

Time 0ms   - Fire validateRequestAction
Time 250ms - Ajax request occurs
Time 251ms - Fire validateCancelAction
Time 501ms - validateCancelAction passes debounce and cancels properly
Nothing else occurs

Scenario 2 (broken)

Time 0ms   - Fire validateRequestAction
Time 250ms - Ajax request occurs
Time 251ms - Fire validateCancelAction
Time 400ms - Ajax returns, APISuccessValidate action fired
Time 501ms - validateCancelAction passes debounce and there is nothing to cancel

Is there a way I can write my epic such that only the validateCancelAction can bypass the debounceTime and cancel the ajax call without waiting?

Thanks!

2 Answers

I assume that there're two scenarios that you may want it to be:

Scenario 1:

You want to cancel the throttle immediately when cancel action is received. This means that you may want to reset the second stream. It is good but may be not what you want.

action$ => {
  const requestAction$ = action$.pipe(
    ofType(validateRequestAction),
    share()
  )
  return merge(
    action$.pipe(
      ofType(validateCancelAction),
      mapTo(false)
    ),
    requestAction$.pipe(mapTo(true))
  ).pipe(
    distinctUntilChanged(),
    switchMap(
      condition => 
        condition ? 
          requestAction$.pipe(
            debounceTime(250),
            switchMap(query => sendRequest(query)
          ) : EMPTY
    )
  )

Scenario 2:

You send a cancel signal and at the same time, tell every pending requests that: "Hey, you are not allow to dispatch". There're two way to do this:

  • The first, throttle the cancel action with the same latency with the request action so that it race against request action stream.

Code:

merge(
  action$.pipe(
    ofType(validateCancelAction),
    debounceTime(250),
    mapTo(undefined)
  ),
  action$.pipe(
    ofType(validateRequestAction),
    debounceTime(250),
    pluck('payload')
  )
).pipe(
  switchMap(
    query => query ? sendRequest(query) : of({ type: validateCancelDone })
  )
)
  • The second and the correct solution is, when a cancel action is dispatched, set the state to being cancelled. Every throttled actions have to check this condition before it is allowed to make any request:

Actually, this is just whether you want to store the cancelled state inside your stream or inside redux. I bet you choose the first one. Code:

export default action$ => 
  combineLatest(
    action$.pipe(
      ofType(validateRequestAction),
      debounceTime(250),
      pluck('payload')
    ),
    merge(
      action$.pipe(
        ofType(validateCancelAction),
        mapTo(false)
      ),
      action$.pipe(
        ofType(validateRequestAction),
        mapTo(true)
      )
    ).pipe(
      distinctUntilChanged()
    )
  ).pipe(
    switchMap(
      ([query, allow]) =>
        allow
          ? sendRequest(query)
          : EMPTY
    )
  )

Edit:

You also need to distinctUntilChanged() the allow stream or debounceTime will take no effect.

Related