Building an Angular application with a reactive style using RxJs often leads developers into a few well-known traps. Below, we examine some of the most frequent problem scenarios, break down why they occur, and show how to resolve them. If you have experience with RxJs, try to diagnose each situation before reading the solution—just for practice.
Pitfall 1 - Nothing Happens
Suppose we write a service method designed to save all data to the backend when a button is clicked:
We intend to connect a messages component and an error handling service later to display any possible errors, but for now we only trigger the save operation through a click handler:
With that, the Save feature seems complete. But when we test it, can you predict the outcome?
...
Absolutely nothing occurred—funny, right?
After some debugging, we confirm that the method itself is invoked. So why is there no visible effect?
Pitfall 1 Explanation and how to avoid it
An observable is essentially a blueprint for managing a stream of values, akin to a function. Creating an observable is similar to declaring a function—the declaration alone doesn't execute any logic. Just as defining a function doesn't run its body, we must call the function to make something happen. With observables, this means we need to subscribe to it for execution to begin.
To fix this issue, we can make a change at the service level:
Notice that we now return the observable we created, whereas previously the observable was created and the method exited without any subscription.
In the calling component, we can subscribe to this observable:
With an active subscription, the HTTP lesson creation POST request will be sent to the backend as expected, resolving the issue.
To prevent this problem, we must ensure that any observables returned by the HTTP service are subscribed to—either directly or through the async pipe.
However, sometimes the trouble isn't a missing HTTP call, but rather the opposite: too many calls are sent.
Pitfall 2 - Multiple HTTP requests
Let's try using the async pipe, passing it an observable we retrieve from the service layer. This observable will emit a list of lessons:
But we also want error handling in case the call fails:
So we add a subscription to manage errors and feed the observable to the async pipe. What might be wrong here?
At first glance, nothing seems off—we test it and everything works. Yet, for some reason, you check the network tab in Chrome Dev Tools.
And what do you see?
... /lessons GET 200 OK
... /lessons GET 200 OK
That's right—duplicate HTTP requests! The data retrieval request is sent twice. What explains this?
Pitfall 2 Explanation
Remember that an observable is just a definition, much like a function declaration. If we subscribe to it multiple times, each subscription triggers a new HTTP request—one per subscription.
This parallels calling a function multiple times, where each call yields a separate return value.
So, subscribing more than once to an observable from the Angular HTTP Client results in multiple HTTP requests!
Our intention wasn't that—we wanted one HTTP request, emit the data, handle errors if any, and otherwise display the data. Instead, what happens is:
- One request returns, and the data is shown on screen.
- A second request is sent to check whether an error message should appear.
In this setup, if errors are intermittent, it's possible for the first request to succeed while the second one fails with an error.
How to fix Pitfall 2
How do we resolve this? One effective approach for this scenario is:
The shareReplay RxJs operator elegantly addresses unintended multiple HTTP requests and was introduced specifically with HTTP requests in mind.
This operator ensures that only one HTTP request is made upon the first subscription, and the result is then served from memory to any subsequent subscribers.
No additional HTTP requests occur as long as we keep subscribing to the same observable initially received from the service.
That's typically what we expect from data modification observables from the service layer: the operation should happen once, and the result can be shared with any view subscribers.
We wouldn't want the data modification HTTP request to repeat for every view subscriber—since the saved data is identical, that would be redundant.
Keep this multiple subscription issue in mind when building service layers with the HTTP module.
Consider applying the shareReplay operator as a practical solution for many observables returned by an Angular HTTP-based service; it will prevent many unintended HTTP requests across your application.
Another situation, more common with libraries like AngularFire than with the HTTP module, concerns observable completion.
Pitfall 3 - Router gets Blocked?
Imagine you're building a Router Data Resolver using AngularFire, not the Angular HTTP Library. With the HTTP library, this issue wouldn't occur, as we'll see shortly.
This router example illustrates a broader problem that could surface in other contexts. The purpose of a data resolver is to load data before the routing transition completes, ensuring the data is ready when the new component renders.
Here's a sample router configuration for a data resolver:
If we navigate to /edit, we'll load an EditLessonComponent, but first we must load data: a list of lessons. For this, we define a router data resolver:
Once the resolver is implemented, we attempt to trigger the router transition where it's applied, and then guess what happens?
...
Again, nothing occurred! The router transition didn't happen; we remain on the original screen, and the UI doesn't update. It's as if the transition was never initiated. What could have gone wrong?
Pitfall 3 Explanation
This relates to how both the router and the AngularFire observable API function. The router subscribes to the observable we return, waits for data to arrive and for the observable to complete, and only then triggers the route transition.
That's because the router needs the data available before instantiating the target component.
After all, the main role of a data resolver is to fetch data so it's ready when the target component is created.
Thus, when we return an observable from a data resolver, we must ensure it not only fetches data but also completes, so the router can confirm the data is ready.
The router assumes the observable may emit multiple values; it waits for the observable to either complete or error out before finishing the routing transition.
But how do we guarantee our observable always completes? In this case, we could use the first() or take(1) operators to create a derived observable that completes after the first value:
Always keep Observable completion in mind
With this adjustment, route navigation completes successfully. This router example is just one way that overlooking observable completion can cause issues.
Completion matters in various observable-related scenarios, such as with the RxJs concat operator: if we pass it an observable that never completes, the concatenation won't happen.
With the Angular HTTPClient module, observables always emit one value and then complete (unless they error out), so the issue above wouldn't appear with HTTP Client Observables.
But in our case, since AngularFire observables are designed to continuously emit server-pushed data changes from the realtime database, they don't complete after a single value.
Consequently, because the observable won't complete, the router will "hang" the routing transition, and the UI won't show the target component.
The solution is to ensure data resolver observables complete properly, which happens implicitly with HTTP Observables but not with AngularFire Observables, as Firebase observables are long-lived.
Summary
These are among the most prevalent RxJs pitfalls when building Angular applications:
- Forgetting to subscribe, causing nothing to happen, as with HTTP Observables.
- Subscribing too many times unintentionally or failing to share execution results with subsequent subscribers—this can easily happen with HTTP Observables.
- Forgetting to complete: this isn't an issue with HTTP observables since they emit one value and complete or error out, but it's prone to occur with long-lived observables.
If you find yourself in a situation where something isn't working as expected, asking these questions will help pinpoint the solution:
- Has this observable been subscribed to?
- How many subscriptions does this observable have?
- When does this observable complete—does it complete at all?
- Do I need to unsubscribe from this Observable?
We hope you found this post useful. Check the list below for other related posts and Angular resources.
Subscribe to our newsletter to be notified when more posts like this are published:
If you want to dive deeper into RxJs, we suggest the RxJs In Practice Course, which covers many useful patterns and operators in detail.
For using RxJs specifically in Angular applications, we recommend the Reactive Angular Course, which covers common reactive design patterns for building Angular apps.
If you're new to Angular, check out the Angular for Beginners Course:
More Angular reads
If you found this useful, you might also enjoy some of our other popular content:
- Angular Router - Building a Navigation Menu with Bootstrap 4 and Nested Routes
- Angular Router - A Deep Dive and Common Mistakes to Avoid
- Angular Components - Core Concepts
- Building Angular Apps with Observable Data Services - Common Pitfalls
- Angular Forms - Template-Driven vs Model-Driven Approaches
- Angular ngFor - All Features, trackBy, and Why It Works With More Than Just Arrays
- Angular Universal - Creating SEO-Friendly SPAs
- Angular Change Detection - An In-Depth Look
- TypeScript Type Definitions - Npm, @types, and Compiler Opt-In Types Explained
