In a response to RxJS: Avoiding switchMap-related Bugs, Martin Hochel mentioned a classic use case for
switchMap. For the use case to which he referred,
switchMap is not only valid; it’s optimal. And it’s worth looking at why.
Dealing with stale results
Let’s look at an example that involves an expensive call to a backend service: a search for addresses that match a partial address typed into an HTML
Here’s the NgRx effect:
public findAddresses = createEffect(() => this.actions$.pipe( ofType(LocationActionTypes.FindAddresses), map(action => action.partialAddress), debounceTime(400), distinctUntilChanged(), switchMap(partialAddress => this.backend .findAddresses(partialAddress) .pipe( map(results => new FindAddressesFulfilled(results)), catchError(error => of(new FindAddressesRejected(error))) ) ) ));
And here’s the
const findAddresses = actions$ => actions$.pipe( ofType(actions.FIND_ADDRESSES), map(action => action.partialAddress), debounceTime(400), distinctUntilChanged(), switchMap(partialAddress => backend.findAddresses(partialAddress).pipe( map(results => actions.findAddressesFulfilled(results)), catchError(error => of(actions.findAddressesRejected(error))) ) ) );
The effect/epic debounces the user input so that backend searches are not performed for each keystroke and uses
distinctUntilChanged so that no searches are performed unless the partial address has changed. The operator that’s then used to flatten the backend observable is
From the summarised recommendations in RxJS: Avoiding switchMap-related Bugs, we know that:
concatMapcould be used as a conservative choice;
mergeMapshould not be used — the ordering of the results is important;
switchMapcould be used — when a new search is made, pending results are no longer needed; and
exhaustMapshould not be used — searches for new, partial addresses should not be ignored.
So how would the behaviour differ if the effect/epic were to use the conservative choice of
concatMap rather than
Whether or not the behaviour differs depends upon two things:
- the time it takes for the backend searches to be fulfilled; and
- the time between the user’s keystrokes.
debounceTime operator imposes a limit on how frequently backend searches can occur, so unless the fulfilment of a search takes longer than the debounce time, there will be no difference in behaviour between flattening with
concatMap and with
switchMap — regardless of how slowly the user types.
Similarly, there will be no difference in behaviour if the user types so slowly that each search is fulfilled before the next keystroke occurs.
Let’s look at the worst-case scenario: the searches take significantly longer than the debounce time to be fulfilled and the user types slowly, with intervals between keystrokes that slightly exceed the debounce time.
In the worst-case scenario, if
concatMap is used, each keystroke effects a search and the results of that search will still be pending when the next key is pressed — so the next search will be queued. This sees the displayed search results become increasingly out-of-sync with the typed-in partial address and, as each of the searches has to be fulfilled before the next is performed, it might take some time for the displayed results to become consistent with the
switchMap, each keystroke still effects a search, but any pending search will be aborted. So, in the worst-case scenario, the displayed search results will become consistent with the input more quickly than if
concatMap were to be used, as they’ll be consistent as soon as the pending search is fulfilled.
When choosing a flattening operator for an effect/epic, if subsequent actions of the same type will render pending results stale,
switchMap is unequivocally the best choice.