RxJs Observable - Defer

Angular FormGroup valueChanges: Deferring Observable


6 min readPublished

Developers who are working with Angular will eventually work with Angular reactive forms. In reactive forms, often developers will need to listen to the changes in form value and then react based on it by listening to the Observable.valueChanges property of a reactive form like the following:

1this.observableOutput$ = this.form.valueChanges.pipe(
2  startWith(this.form.value),
3);
component.ts

And then in the component template:

1<div *ngIf="isComponentVisible">
2  {{ observableOutput$ | async }}
3</div>
4
component.html

Now I have an example of such code in the following Angular page: NgDeferFormValueChanges (christoment.github.io), compiled from my GitHub repository.

This Angular app has a hardcoded list of fruits that will be filtered by name using the search input given from the user. The app is also showing the filtered down list using three different techniques.

Have a play around for a minute in the above example page, I'll wait here!

...

...

Welcome back!

Did you find out what is happening and what is the problem?

The Problem - Recap

Congratulations if you've find out what's the problem and what is the root cause! If you haven't find the root cause yet, let's continue our discussion so we can learn what happened.

If you type into the given text box on the top, the list on Example 1 will be filtered correctly as expected.

NgDefer - Example of observing Angular form's valueChange
NgDefer - Example of observing Angular form's valueChange

However, we can observe an interesting event when we show Example 2 and Example 3 using the "Show Component" button. The list on Example 2 is different than Example 1 and Example 3. What is going on?

NgDefer - Inconsistent list when showing hidden components
NgDefer - Inconsistent list when showing hidden components

Now let's try to type more key to the search bar, let's say adding another character - 'p' - to the search so the search keyword is now "App". Notice that now all examples is showing the same list!

NgDefer - All examples are now showing the right list
NgDefer - All examples are now showing the right list

Okay, so now the end result is exactly what we are expecting, but what happened in the middle of the step where we showed Example 2 and Example 3?

To understand this, we must first look into what is RxJs doing with startWith and when does the subscription actually happen.

Observable Initialisation Timing

The Example 1 and Example 2 is reading an observable which is constructed using the simple valueChanges listener with RxJs startWith operator, since valueChanges does not emit the initial value of the form control. Example 3 is built using the same logic but it is wrapped in an RxJs function defer.

1// Example 1 and 2
2this.exampleResult$ = this.form.valueChanges.pipe(
3  startWith(this.form.value),
4  map((value) => this.candidates.filter((candidate) => !value.search || candidate.toLocaleLowerCase().indexOf(value.search.toLocaleLowerCase()) >= 0))
5);
6
7// Example 3
8this.exampleDeferResult$ = defer(() => this.form.valueChanges.pipe(
9  startWith(this.form.value),
10  map((value) => this.candidates.filter((candidate) => !value.search || candidate.toLocaleLowerCase().indexOf(value.search.toLocaleLowerCase()) >= 0))
11));
12
app.component.ts - Simple Observable

What is the difference between using defer and not using it?

In the first scenario, we are constructing the observable exactly right there, at that line of code. In the second scenario where we are using defer, we create an observable factory function which will be called later when this observable is subscribed.

The former strategy works well for Example 1, which subscribes to this observable since the beginning of the component. Example 2, however, subscribes to this observable later somewhere along the component lifecycle, where the user might already have changed the input keyword. Since we already created the observable in the beginning, startWith is built using the initial this.form.value, and does not reflect the actual value of the form in the run time.

The later strategy where we uses defer for Example 3 is designed exactly for this scenario. Instead of constructing the startWith value in the beginning, we evaluate the value exactly when the observer need it. Therefore, it will have the up-to-date form value!

In summary, the root cause of the problem in the example Angular application above is that the startWith operator was only evaluated once - which happens when we are constructing the observable. With the Example 3 however, the startWith operator is evaluated later only when it is actually subscribed by the observer.