Learn what not to do with RxJS in your Angular applications.
Dependent Actions
This has come up a lot for me. I need to wait until I have A before I can request B. You might say that the request for B is dependent on the response of A.
Here is the anti-pattern:
public courses: Observable<Array<Course>>;
ngOnInit() {
this.store.select(getUser())
.takeWhile(() => this.alive)
.first()
.subscribe(user => {
this.store.dispatch(new LoadCoursesForUserAction({ user: user }));
this.courses = this.store.select(getCourses);
});
}
Let me explain what I am trying to accomplish:
- First, I have a public property
courses
that is an observable of an array ofCourse
objects. - I use
takeWhile()
to complete the subscription while the component is alive. I set the value ofalive
to false in thengOnDestroy()
lifecycle method. - I then use the
first()
method to complete the subscription after the first value is emitted. I am only interested in getting the single user that is authenticated. - Then, I subscribe to the observable using the
subscribe()
method. My observer is defined using a fat arrow function, with a singleuser
argument. - Within the observer function I
dispatch()
a new action to load the courses for the user, aptly namedLoadCoursesForUserAction
, providing theuser
object. - Finally, I set the courses property to the the array of courses that is in the store using the
getCourses()
selector function.
It works.
I know we’ve all heard those words, and if we’re honest with ourselves it is likely that we have uttered those same words.
While, it works. There is a better, more concise way to working with dependent actions.
Let’s look at, what I think, is a better approach:
public courses: Observable<Array<Course>>;
ngOnInit() {
this.courses = this.store.select(getUser())
.do(user => this.store.dispatch(new LoadCoursesForUserAction({ user: user })))
.switchMap(() => this.store.select(getCourses));
}
Let’s review the update code:
- First, notice that I am setting the value of the
courses
property to the result of theswitchMap()
operator. - I use the
do()
operator to tranparently perform an action (or side effect), in this case, todispatch()
theLoadCoursesForUserAction
action. Thedo()
operator will receive theuser
that is emitted from the observable that is created using theselect()
method, specifying thegetUser()
selector function. - Finally, using the
switchMap()
operator, the inner observable (obtaining the user) is complete, returning the observable of the array ofCourse
objects.
What are the benefits?
- We don’t need to deal with subscribing and unsubscribing.
- It’s more concise.
- We take advantage of provided RxJs operators.
Ok, what if we have multiple dependencies, such as:
- C depends on B
- B depends on A
Here is an example of the anti-pattern:
public administrator = false;
public group: Observable<Group>;
public users: Observable<Array<User>>;
ngOnInit() {
const PARAMS_ID = "id";
// get id route param
this.activatedRoute.params
.takeWhile(() => this.alive)
.subscribe(params => {
// get the id parameter
const id = params[PARAMS_ID];
// get the group
this.store.dispatch(new LoadGroupAction({ id: id }
this.group = this.store.select(getGroup);
this.group
.takeWhile(() => this.alive)
.filter(group => !!group)
.first()
.subscribe(group => {
// load users in group
this.store.dispatch(new LoadUsersInGroupAction({ group: group }));
this.users = this.store.select(getUsersInGroup);
// set adminstrator
this.store.select(getAuthenticatedUser)
.takeWhile(() => this.alive)
.first()
.subscribe(user => {
this.administrator = (group.security.administrators.indexOf(user._id.toString()) > -1);
});
});
})
}
In this example, the dependencies are:
- I need the user and the group to determine if the user is an administrator of a group.
- I need the group to retrieve the users in the group.
- I need the route param to get the group based on the id value.
Some of the problems here are:
- There are a lot of nested subscriptions.
- This is somewhat complicated and difficult to read.
- I am using the deprecated
params
observable when I should be using theparamMap
observable ofActivatedRoute
.
Here is a better approach to this problem:
public administrator = false;
public group: Observable<Group>;
public users: Observable<Array<User>>;
ngOnInit() {
this.group = this.activatedRoute.paramMap
.takeWhile(() => this.alive)
.do(params => this.store.dispatch(new LoadGroupAction({ id: params.get(PARAMS_ID) })))
.switchMap(() => this.store.select(getGroup));
this.users = this.group
.do(group => this.store.dispatch(new LoadUsersInGroupAction({ group: group })))
.switchMap(() => this.store.select(getUsersInGroup));
this.administrator = this.group
.switchMap(group => this.store.select(getAuthenticatedUser)
.switchMap(user => Observable.of(group.security.administrators.indexOf(user._id.toString()) > -1))
);
}
There a few reasons why I like this better:
- It’s shorter and more concise.
- I can group together logical blocks of code making it more readable.
- There are no nested subscriptions with lots of indenting.
- I am using the recommended
paramMap
observable.
Avoid Duplicate Requests
The AsyncPipe is perhaps one of the coolest things in Angular. I can remember sitting at ng-conf in 2016 watching a presentation on Angular (then called Angular 2) and the thunderous applause after the presenter demoed the async pipe. For those of us that have been doing asynchronous coding in JavaScript for awhile, it was like the holy grail. It was pure magic.
But, as I grew in the ways of Angular I began to notice something odd. If I had defined an Observable in my component’s TypeScript file, and then had multiple child components with input bindings using the async pipe, I noticed there were multiple network requests for the same endpoint.
Let me explain in code. Here is my component:
public course: Observable&l;Course>;
ngOnInit() {
const PARAMS_COURSE_ID = "courseId";
// set course
this.course = this.activatedRoute.paramMap
.takeWhile(() => this.alive)
.do(params => this.store.dispatch(new LoadCourseAction({ id: params.get(PARAMS_COURSE_ID) })))
.switchMap(() => this.store.select(getCourse))
}
And here is my HTML template:
<ama-module-menu> [course]="course | async" ></ama-module-menu>
<ama-module-content> [course]="course | async ></ama-module-content>
A few things to note:
- First, in my component’s
ngOnInit()
lifecycle hook I am setting the value of the publiccourse
property to an observable. It happens to be that the value is coming from an NgRx store using a selector,getCourse()
, using theswitchMap()
operator. - In my template I have an
<ama-module-menu>
element that has an input binding forcourse
, which uses the async pipe. (P.S. just for fun, “ama” is an acronym for ‘a million-dollar app’). - In my template I also have an
<ama-module-content>
element that also has an input binding forcourse
, which also uses the async pipe.
The problem: a quick review of the network traffic shows that the async pipe will create multiple subscriptions, resulting in multiple HTTP OPTIONS requests for the same resource. Interestingly, it seems that the HttpClient module in Angular will cancel the multiple GET requests, and will on issue a single GET request.
The solution, using the share()
operator:
share(): Share source among multiple subscribers.
The share()
operator is very easy to use, and allows us to avoid the duplicate HTTP requests when using multiple async pipe bindings in our templates.
We simply need to invoke the share()
operator when setting up our observable:
const PARAMS_COURSE_ID = "courseId";
// set course
this.course = this.activatedRoute.paramMap
.takeWhile(() => this.alive)
.do(params => this.store.dispatch(new LoadCourseAction({ id: params.get(PARAMS_COURSE_ID) })))
.switchMap(() => this.store.select(getCourse))
.share()
}
Note, there is only one difference between our component code before and this code above: we add on the invokation of the share()
operator.
The results in a single HTTP GET request for our multiple async pipe subscriptions in our view template.
Thank you RxJs!
Using *ngIf
Updated: 2017-11-26
As Kimberly pointed out in the comments below, you can also use *ngIf
result binding to avoid duplicate requests:
[course]="c" > [course]="c > ``` This avoids the need to use the `.share()` operator for multicasting with RxJs. Thank you Kimberly for sharing this solution! ## More? These are just two antipatterns that I have observed (haha). Please share any other antipatterns that you have learned from!