0

I have a list of invites in my app, each one with corresponding delete button. When a user clicks delete a DELETE_INVITE action is dispatched and an epic fire:

const deleteInvite = (action$: any, store: Store<ReduxState, *>) =>
  action$.pipe(
    ofType(DELETE_INVITE),
    mergeMap(({ payload }) =>
      ajax(api.deleteInvite(payload.inviteId)).pipe(
        map((response: Object) => ({
          type: DELETE_INVITE + SUCCESS,
          payload: {
            data: response.response,
            status: response.status,
          },
        })),
        catchError((error: Object) => of({
          type: DELETE_INVITE + FAILURE,
          error: {
            response: {
              data: error.xhr.response,
              status: error.xhr.status,
            },
          },
        })),
      ),
    ),
  );

Now I would like to ensure that only one request is fired at the time and wait until last one finished. In other words, I want to protect myself from a situation where user rapidly clicks all over the buttons and fires few request simultaneously.

switchMap is kind of something I'm looking for because it would handle only most recent click... but the request would be already fired and UI left with outdated data. So I need something that will let call mergeMap again only when inner chain completes.

Tomasz Mularczyk
  • 34,501
  • 19
  • 112
  • 166

2 Answers2

1

I guess I would ask why you need to use redux-observable to achieve this in the first place. Can't you just set some state variable in your redux store at the start of your request (like deleteInProgress = true), and use this state to disable the delete button. And when your request finishes (either successfully or erroroneously), set the deleteInProgress flag back to false, which will re-enable the button.

tlfu
  • 171
  • 4
  • I could do that. I could even set it in redux and use this flag inside mergeMap to decide what to return... but I think `rxjs` was made to handle async stuff like this in a first place, so I wonder if there is some nice declarative way to say "Hey, wait until inner observable complete before starting another one". But I'm not an expert and I can miss something fundamental. – Tomasz Mularczyk Feb 22 '18 at 11:04
  • Did you see this thread? (https://stackoverflow.com/questions/44667352/rxjs-throttle-fetch-until-request-has-finished?rq=1) Sounds like a very similar use case, and [jayphelps](https://stackoverflow.com/users/1770633/jayphelps) (one of the redux-observables authors) suggests a solution. – tlfu Feb 22 '18 at 11:10
  • thanks, I haven't seen that. It seems that I need to keep a flag somewhere then... – Tomasz Mularczyk Feb 22 '18 at 11:14
1

Based on your comments below, it sounds like what you want is exhaustMap.

Projects each source value to an Observable which is merged in the output Observable only if the previous projected Observable has completed.

const deleteInvite = (action$: any, store: Store<ReduxState, *>) =>
  action$.pipe(
    ofType(DELETE_INVITE),
    exhaustMap(({ payload }) =>
      ajax(api.deleteInvite(payload.inviteId)).pipe(
        map((response: Object) => ({
          type: DELETE_INVITE + SUCCESS,
          payload: {
            data: response.response,
            status: response.status,
          },
        })),
        catchError((error: Object) => of({
          type: DELETE_INVITE + FAILURE,
          error: {
            response: {
              data: error.xhr.response,
              status: error.xhr.status,
            },
          },
        })),
      ),
    ),
  );
jayphelps
  • 15,276
  • 3
  • 41
  • 54
  • Thanks! but I was looking for a way to _ignore_ incoming values until inner observable completes... – Tomasz Mularczyk Mar 09 '18 at 10:30
  • thanks! that's what I was looking for and somehow missed in docs. Would you rather disable button to prevent a user from firing few exact requests at a time or used `exhaustMap` in an epic? – Tomasz Mularczyk Mar 10 '18 at 11:06
  • 1
    @Tomasz really depends. If I was going for code simplicity/ease I would just disable the button. If I was going for best UX I would actually do "optimistic updates", which means from the UI perspective I would make it appear the delete was instant, and only give them negative feedback if for some reason it fails (assuming that was rare). While I use a custom middleware for this for various reasons, this one is also pretty good for that: https://github.com/ForbesLindesay/redux-optimist – jayphelps Mar 14 '18 at 03:03
  • 1
    `exhaustMap` usage is pretty rare in practice as dropping new attempts to do something just isn't something that is usually done. But certainly you know your app better than I, so potentially it is the right choice! – jayphelps Mar 14 '18 at 03:05
  • 2
    One important thing to keep in mind if you use `exhaustMap` (or really any of these flattening operators) is what you want to happen if two unrelated but concurrent requests come in. e.g. two invite deletes for _different_ invites. You probably don't want to drop ones that aren't for the same invite! In those cases you might need to do some sort of `groupBy` – jayphelps Mar 14 '18 at 03:06
  • Wow. Thanks a lot for detailed answer! I will reconsider your suggestions. – Tomasz Mularczyk Mar 14 '18 at 08:17