There exists no better way to learn anything than from experiencing pain. It's a humble yet loud teacher.
Last week, I reviewed code of a project transitioning their state management to NgRx. Looking into the NgRx Effects, I noticed similar pitfalls with RxJs that if not understood well will leave you scrambling for answers. Since my team and I fell victim to these misunderstandings, I hope to alleviate you of these common pitfalls.
Pitfall #1 - HttpClient Cancelling - switchMap
I trudged through the creation of modeling the state, selectors, reducer, actions, and side effects. Side effects trigger a stream to provide new actions based on external interactions. Web apps communicate with back end services to store state as a source of truth for the different entities within an app. Many of these interactions call REST services.
Side effects occur when an app’s internal state occurs outside of the local environment. In my case, I communicate with a REST service as the source of truth for state.
As I called these REST service from the different actions, I created many effects like below.
This issue reared its ugly head when I dispatched the action above multiple times.
Can you guess what happened?
As I watched my network requests pile in through Chrome's Network tab, the previous requests canceled.
switchMap cancels the previous observable and subscribes to the new observable. As new
FooRequestByIdAction dispatches, the stream cancels the previous HTTP requests.Valid instances arise using
switchMap and HTTP requests in effects. For instance, as a user enters their username into a textbox, an HTTP request is made to check the existence of the username. As the user keys in each letter, the previous request may be in flight. In this case, using
switchMap cancels the previous HTTP request as the concern is only for the last request.
mergeMap uses the inner observable projection to the next operator in the effect's stream. It does not cancel the previous observable, allowing our HTTP request to finish, and makes multiple requests at once.
Pitfall #2 - Effect Unsubscribed - catchError
As noted earlier, multiple HTTP requests dispatched for different entities needed by the components. As my project's code base grew for the number of entities, I quickly noticed issues dispatching actions again if the HTTP request failed. While the backend services were developed in parallel, this issue surfaced as REST endpoints changed causing HTTP errors.
Say my component required multiple
Foo models, and I dispatched multiple
Using Redux Dev Tools in Chrome, I found two of the three actions fired and updated my store. The third action dispatched. I received an HTTP request Error, and the
FooActions.RequestError dispatched once. I expected the fourth action to dispatch, but I found nothing in my Redux Dev Tool with
I followed a pattern for my effects below of
mergeMap of the HTTP response,
map success action to save in the store, and
catchError in the effect's stream.
Did you catch what will happen if the rest client throws an error?
catchError returns a new observable
of(new FooActions.RequestError(err)). When
catchError operator calls, it creates a new observable, and in this case an observable that completes the effect's stream using the
of operator and the effect no longer will pick up the action.
Solution: Move the catchError into the inner observable
To fix this, the inner observable on the
httpClient handles catching the errors. This observable will emit the correct action based on the response.
Pitfall #3 - Handling User Logouts - takeUntil
I've written a few Firebase applications and enjoyed the real time updates Firestore allows. The rules within Firestore determine how records are queried and written. Below, I created a rule that checks if the current request’s authentication userId matches the userId in the user-profile path in the
Based on the rule, a user may only read and write this record if the
isUsersDocument(userId), which checks the current user and
When a user logged into my system, I wrote an effect that watches for changes in the
The stream remains open after we dispatch a
QUERY_USER_PROFILE action. If the present user were to log out, how do I stop the inner stream from Firebase emitting values? If another person logs in on the same browser, they could view the previous user's information.
My application dispatches a
LOGOUT_USER action. I watch for certain actions being dispatched via
this.actions$.pipe(ofType(LOGOUT_USER)). So now the question is, what operator do I use within my inner observable that can consume and stop the
user-profile value changes stream?
Solution: takeUntil operator watching the action stream
takeUntil operator consumes the observable provided to it and on the emit of the provided observable stops the outer stream.
Understanding RxJs Operators
RxJs provides a rich library with a steep learning curve. Take a moment to understand which operators change or complete streams. You will save yourself the pain points I’ve experienced.
- catchError - From my examples, catchError changes the stream and completes with an of operator.
- switchMap - cancels previous observable and switches to the new observable