Welcome to today’s post.
In today’s post I will be discussing what the SwitchMap() function is in RxJS and when we can use it within an Angular component to control the flow of observables from user interaction to resulting output.
In a previous post I discussed the SwitchMap RxJS operator and used it is an application use case where we took two observables that were two independent Http Web API REST calls. We then saw how to pipe the observables into values that were then bound to local properties within the component.
When to use Switchable Observables
Aside from using the SwitchMap operator in the above case, we can also use SwitchMap when both observables are dependent on one another. This dependency is likely to be data dependent, where the outer observable provides an output response that is an input to an inner observable.
The main benefit of a switchable observable is that we can keep the original outer observable stream and pass that stream as an input observable to the inner observable without having to create two separate observables, which will consume more memory and leak memory within your Angular application. Despite this, your application will still need to make a separate HTTP request if each observable is a HttpClient request to a Web API HTTP REST URI.
Typical and quite common use cases include:
- A drop-down list with and another drop-down list that depends on the selected value of the first drop-down list.
- A login service that provides a response that includes an access token and a username or login and a list of user role memberships that depend on the username or login.
In today’s post I will focus on the second scenario.
In a login service, the login function posts a request to authenticate the user with the username and password, then returns the user login name through a subscription.
Using Nested Subscriptions
We then request the user roles for the given user login through another dependent subscribe within the action of the outer subscription.
While this pattern of nesting and subscribing to observables works, there is a good reason why it is not considered best practice. Suppose we have the following nested observable subscription:
let subscription1, subscription2, subscription3;
let subscription1 = apiService.method1().subscribe(res1 =>
{
let subscription2 = apiService.method2(res1).subscribe(res2 =>
{
let subscription3 = apiService.method3(res2).subscribe(res3 =>
{
// some actions
});
});
}
If the outer subscription is successful and one of the inner subscriptions is not, then keeping track of what subscriptions were created and which ones were not can be a very challenging task if the nested subscription were called many times over the course of an application session. In addition, if we don’t free the inner subscriptions that were created then there will be memory leaks which can degrade application performance very quickly.
Suppose our login method is as shown:
login(userName: string, password: string) {
var config = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
})
};
this.http.post<any>(this.apiURL,
{ "UserName": userName, "Password": password }, config)
.subscribe(
res => { this.authenticate(res)
this.apiService.getUserRoles(this.currLoggedInUserValue)
.subscribe(r =>
{
this.authenticateRole(r);
console.log("getUserRoles(): user " +
this.currLoggedInUserValue +
" is member of " +
(r ? r.length: 0) + " roles.");
this.loginResponse.next("login success");
});
},
error => {
console.error('There was an error!', error);
this.loginResponse.next(error);
},
()=> {}
);
}
We can see that not only do we have nested subscription, but there is also error-handling. There are no variables to keep track of the subscriptions as they are created.
The above will allow a user to login, get authenticated, provide an access token, then use the login id to determine the roles the user is a member of.
But remember the problems I mentioned in relation to the memory leaks caused by failing to release unused subscriptions!
In the next section, we will show how resolve this issue in the next section.
Using Switchable Observables
We can provide a solution to these problems by doing the following:
- Replace each the Subscribe() operator with the SwitchMap() operator.
- Declare a Subscription object within the component.
- Assign our outer observable to the subscription object.
- Destroy the subscription to the outer observable when the component is releases (in an ngOnDestroy()).
Below is the nested switchMap():
let subscription1: Subscription;
let subscription1 = apiService.method1()
.pipe(switchMap(value1 => {
return apiService.method2(value1)
.pipe(switchMap(value2 => {
// some actions
});
});
}
Piecing together all the above recommendations, we have the following:
Our service declaration will look as shown with the OnDestroy implementation and the subscription variable:
@Injectable()
export class AuthService implements OnDestroy {
private loginSubscription: Subscription;
..
The login method with the refactored observables as shown below:
login(userName: string, password: string) {
var config = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
})
};
this.http.post<any>(this.apiURL,
{ "UserName": userName, "Password": password }, config)
.pipe
(
switchMap(value => {
this.authenticate(value);
return this.apiService
.getUserRoles(this.currLoggedInUserValue)
.pipe(switchMap(r => {
this.authenticateRole(r);
console.log("getUserRoles(): user " +
this.currLoggedInUserValue +
" is member of " +
(r ? 0 : r.length) + " roles.");
this.loginResponse
.next("login success");
return r;
}))
}),
catchError((error) => {
console.error('There was an error!', error);
this.loginResponse.next(error);
return throwError(error);
})
)
.subscribe();
}
Note that within the inner observable action delegate, if we forget to return the observable to the enclosed outer observable as shown:
.pipe(switchMap(r =>
{
this.authenticateRole(r);
console.log("getUserRoles(): user " +
this.currLoggedInUserValue + " is member of " +
(r ? r.length: 0) + " roles.");
this.loginResponse.next("login success");
return r;
}
))
Then the following compiler error will occur:
Argument of type '(value: any) => void' is not assignable to parameter of type '(value: any, index: number) => ObservableInput<any>'.
Type 'void' is not assignable to type 'ObservableInput<any>'.ts(2345)
Below is the implementation to release the subscription:
ngOnDestroy()
{
if (this.loginSubscription)
this.loginSubscription.unsubscribe();
}
When the above is run and we reach the outer observable and the inner observable, an output value is created for each response. Below we see the output for the outer observable:
And below we see the output for the inner observable:
As you can see, we have taken our nested subscribed observable, and applied the RxJS switchMap() operator to not only improve the memory handling of the subscriptions, but to flatten out the observable so that there is only one subscribed observable stream that we have to maintain and release when our application clean up occurs.
That is all for today’s post.
I hope you have found this post useful and informative.
Andrew Halil is a blogger, author and software developer with expertise of many areas in the information technology industry including full-stack web and native cloud based development, test driven development and Devops.