Briebug Blog

Sharing our thoughts with the community

With NgRx Effects, There's A Place for Every Mapper!

With NgRx Effects, There's A Place for Every Mapper!


Mapping operators. If you have been doing NgRx for any amount of time, you are probably aware of the contention that often surrounds which mapping operator should be used in effects. Contention, and confusion, and an oft-changing roadmap. 


Every mapper in its place with NgRx Effects!


We started out with switchMap the quintessential observable mapping operator in rxjs. This simple operator allows us to “switch” streams of notifications from observables, changing from an original stream…an original observable…to a new or different observable.


We then learned that switchMap had some potential issues. It cancels prior switched-to observables when new notifications come through on the original. So we started using exhaustMap, which would not cancel any current or “in-flight” observable. This was great, as most often we tend to make requests to web APIs in our effects, and we don’t want to always be canceling prior requests in favor of subsequent requests.


Of course, we then learned that exhaustMap wasn’t all that great all the time either! Instead of canceling in-flight requests, it would simply drop subsequent requests until the prior was complete. Gone. Forever. So we started using concatMap.



This is it! This is the god mapper! The one that will solve all our problems. It won’t cancel priors nor will it drop subsequents! This will do.



Of course…it didn't… We inevitably learned that concatMap, while it will not cancel or drop anything, and will indeed queue, queueing is not always the best solution either. It can bottleneck high-throughput actions and their effects. So we started using mergeMap, also called flatMap.



THIS has to be the one, right!! Yes! This it! THE mapper. The one mapper to rule them all! The one mapper to bring them all, and in the darkness bind them!!



Er…sorry…waxing Tolkien for a moment there… O_o


Well, mergeMap turns out to work pretty well. It doesn't cancel. It doesn't drop. It doesn't queue. It allows everything to run concurrently. Seems perfect, right! We found our One Mapper! Perhaps…





Every Mapper has its Place

Today, I am going to offer insight. Rather than applaud that the NgRx community has indeed found the perfect mapper…I am here to say that every mapper has its place. There is no One Mapper to rule them all. ;)


Semantics

The key is semantics. Semantics, the meaning of things. Every observable mapping operator in rxjs has unique semantics. Instead of looking for a single mapper that should always be used in every effect, I would propose that each effect should utilize the mapper best suited to the desired behavior.


What does it mean to switch and map? What does it mean to exhaust and map? The meaning of these things can help you choose he best mapping operator for each and every use case. There may be times when switchMap is exactly what you need, and other times when concatMap is just the right tool for the job.



Every mapper has its place. Each mapper is a tool that is ideally suited for particular tasks. As with hammers, which are not the right tool for every carpentry or construction job, so it is with mappers and our reactive code.




Mapping Behaviors

To be capable of choosing the right tool, the right mapper, for the job, you must first understand the unique traits of each and every tool. You must understand what it means to use each one…must understand their semantics. To that end, here are the key traits of each observable mapping operator in rxjs:


  • switchMap: Non-Concurrent; Preemptive; Canceling (subsequent in favor of prior)
  • exhaustMap: Non-Concurrent; Persistent; Dropping (prior in favor of subsequent)
  • concatMap: Non-Concurrent; Queueing; Serial (one at a time)
  • mergeMap: Concurrent; Parallel; Simultaneous (all together)


Fueled with this knowledge, choosing the right mapping operator should be a relatively simple task if you understand the nature of your side effects and their behavior.




Some Use Cases

Let’s run through some use cases to demonstrate that every mapper has its place, and when every mapper is in its place, our side effect behaviors will be correct.


Leveraging Cancellation

Despite its vilification, switchMap still has a place in NgRx effects. While it may not be the most useful mapper for every case, it has its place on occasion. An excellent example would be typeahead implementations. With a typeahead, the goal is to keep the user informed of viable selections they could make as they are typing. As such, acquiring the most recent relevant results as pro-actively as possible becomes critically important. Further, typeaheads are usually used when querying large bodies of information that would not normally fit locally within app state residing in memory in a browser.




In such a context, waiting for a request to complete before allowing another with more recent, more relevant, information is counter-intuitive. Dropping subsequent requests until the prior completes (exhaustMap) could result in stale or even incorrect results.



Waiting for prior requests to complete before trying subsequent requests (concatMap) may eventually result in the correct information being displayed, but may suffer from notable lag.



Further, allowing multiple requests in-flight in parallel is also counter-intuitive, as with parallel requests (mergeMap) there is no guarantee of the order in which said requests may complete, and thus the information displayed to the user may, or may not be, entirely valid.



The ideal mapping operator for our use case here is indeed the one that will cancel prior requests in favor of subsequent requests. The switchMap operator lives again!



Another potential use case for switchMap may be when loading pages of data from a very large data set. To maintain a responsive UI, if a user is rapidly flipping through pages…why waste any time completing requests that are already no longer necessary?



Queueing for Guarantees

Unlike a search typeahead, where the results are generally ephemeral to begin with and prior requests may be working against old & invalid information, there are some operations for which we must guarantee execution in an orderly fashion. Saving and updating information in particular, should be done in an orderly fashion without any unexpected side effects.




In the context of saving changes to an entity, canceling a prior request in favor of a subsequent could lead to problems. This may in particular be true for PATCH-style updates, wherein only modified information is transferred for efficiency. With switchMap an initial save to update a list of child objects with a new child may be dropped in favor of a second save to update a property of that particular child. Since the initial save was canceled, there is no guarantee that this child will exist, and the partial update in the second save will fail.



Further, if we try to use exhaustMap we run the risk that two edits saved in relatively quick succession (which may be the case with auto-saved editing, a common practice with modern apps) will only save the first edit. We may indeed add our new child object to the list, but the subsequent edit to that child could be lost.



Using mergeMap is also non-viable, as while we may indeed get the right results…first adding our child then modifying it, exact order of execution cannot be guaranteed in the case of parallel executions. It is likely that they will execute in proper order, however there is a chance they will not. We may well try to save the child object update first, before it is created.



What we need is to queue our save requests, to ensure that not only is every request executed, but that they are executed in the proper order. The only operator that fits the bill here is concatMap.



Exhausting Idempotence

You may be wondering…what is the use case for exhausting prior requests before considering a subsequent? Idempotent operations, for one! Such is often the case with DELETE requests, where deleting something is more of a guarantee than an operation that can succeed or fail.




While we can use any mapping operator for idempotent operations…it wouldn't necessarily much matter if we switched, or concatenated, or merged... Why waste effort when you do not have to?



With exhaustMap we can perform our action only once, and any additional requests to delete the same thing that come in before our prior request has completed will simply be ignored.



Concurrent Collaborations

So where does concurrency come into play? When might parallelism be a useful tool with reactive state? The potential use cases here could be limitless. Consider a user interface that may load similar information into different parts of its UI, with each set of information requiring a complex server-side operation to construct? Say…reporting, charts and graphs with differing criteria? Perhaps the same report, with different time periods and granularities?




We neither want to cancel prior requests nor cancel subsequent requests, as each and every request matters. However, requesting such report information is often done together, thus the time between identical action dispatches with different criteria is effectively zero. So both switchMap and exhaustMap are non-viable operators here. We could lose information we need.



We could queue the requests. That would guarantee that all are indeed requested, and that we will indeed eventually get the information we require. However, this presents a potential problem from a UI responsiveness standpoint, and each of our UI components will load in an obvious…and serialized…order. Scratch concatMap.



Running the build requests for each report in parallel is the optimal solution. We know that generating each report may take a couple of seconds each, and rather than queue each request up and load them one at a time, let the server churn on them simultaneously so they all drop into the UI as soon as they are ready. Its mergeMap for the win!



Right Tool for the Job

I hope with the above knowledge you may begin to move beyond a dogmatic approach to which mapping operator should “rule them all” towards a more pragmatic approach. Every mapping operator has its place, and when the proper operator is used for each use case, it supports proper behavior.


Our effects are critical pieces of code…we shouldn't be whacking every one with a hammer! Twirling screws with screwdrivers, pounding nails with hammers, cutting boards with saws… Every tool has its place!

Need Support With a Project?

Start experiencing the peace and security your team needs, and continue getting the recognition you deserve. Connect with us below. 

First Name* Required field!
Last Name* Required field!
Job Title* Required field!
Business Email* Required field!
Phone Required field!
Tell us about your project Required field!
View Details
- +
Sold Out