When I first started developing Angular apps, I didn't really understand rxjs. The only time I really used observables is when I needed to make an HTTP call and in that case I would just subscribe to the response and update properties in my component to reflect changes to the user. Now that I have learned the power of rxjs, I see some of the pitfalls of simply subscribing in components. I have enumerated some of those pitfalls below:
1.) Introduce memory leaks.
- Subscriptions must be completed otherwise memory leaks occur. Some subscriptions complete automatically (an HTTP call for instance). However, some must be explicitly completed. When we use rxjs and async pipe in our template, Angular handles completing the subscription for us.
2.) Nested subscriptions. I have seen many code bases that have nested subscriptions where a component subscribes to an observable and inside that subscription, subscribes to another observable (remember callback hell anyone).
3.) Coupling of business logic with presentational logic.
4.) Usually in this pattern, we create some public properties that is updated inside of the subscription which will be used by the template.
5.) Cannot use on push change detection strategy.
- This is because we mutate the state of the component by updating it's properties in the subscriptions. This makes Angular fire the on changes lifecycle hook every time the component changes and not just when an input to a child component changes.
We want to call an API to get list of topics and a list of all interests. Then a user selects a topic, we want to highlight the interests that are pertinent to that topic.
Let's look at the the code below:
Things to note:
- We inject a service that simply returns observables.
- We have several properties on the component typescript file that where we set the value inside of the subscriptions. This means that the component properties are not immutable.
- We have a change event on the picklist that will run business logic on the component to mutate the properties.
- We have 3 subscriptions we create, one of which is a nested subscription.
- This creates possible memory leaks as we need to explicitly complete to the subscriptions (which we do not). There are several ways to do this (with the operators take, takeWhile, takeUntil or calling unsubscribe on the subscription) but we have to remember to do this.
Now let's look at the refactored code below:
Things to Note:
- We remove the app.service injection and instead inject the app-config.service. This service will perform the business logic of you component.
- We remove the previous public properties of the component and instead create a property of type Observable which we get from the app-config.service. This will make the component properties immutable since this new property will be creating a new config (state) object whenever an update needs to be made. This can be seen in the updateInterest method which is called when the topics picklist is updated. The updateInterest method will then call the updateState method in the app-config.service.
- We implement the OnDestroy lifecycle hook to call the unsubscribe method in the app-config.service (more on that later).
Things to Note:
- We reference the config$ observable property in the template. We use async pipe for this property which creates a subscription in the template so we don't have to do it in the typescript file. An advantage of this is that the subscription will automatically complete when the component is destroyed.
- We cast the config$ observable as config. This gives us access to the AppConfig type as it is the same as the line .subscribe(config => ...) when subscribing to an observable.
Things to Note:
- We create a private _config$ behavior subject property. The difference between a subject and a behavior subject is that a behavior subject has a getCurrentValue method that gives us access to the current value of the subject.
- On the getConfig method which is called from the component, we return the behavior subject as an observable.
- We orchestrate the subscriptions in the initConfig method. We get the getInterests observable then switchMap the getAllInterests observable and return an object with the 2 values. Then we use the tap operator, which is used for side effects, to call the next method on the behavior subject. This creates a new instance of the object so it keeps everything immutable still.
- We have an updateState method which is called by the component to call the next method on the behavior subject. Again, this creates a new object, which still keeps everything immutable.
- We also have a getCurrentState method which we can call from the component to get the current value of the observable, which is why we went with the behavior subject instead of the subject.
- We have an unsubscribe method in the service. We call this from the component to complete the subscription we created in this service. Note: this subscription is created in this service and NOT the component.
Going with this pattern for components make for a clear separation of concerns between presentation and business logic. The functional nature of this pattern also makes for more testable code. Finally, the functional aspect of the component allows us to use Angular's on push change detection for the child components. Having public properties in a component that are constantly updated make for buggy behavior when using on push in child components. Being able to use on push is a performance boost since we don't need to constantly check child components' on change lifecycle hook.