Hello Angular Space Community!
As an Angular developer, you may face issues when your component has to use multiple asynchronous sources. Beginner Angular developers may encounter some minor problems with that and might not solve them correctly, so I want to show you a simple trick.
Case Study - Handle multiple Observables inside component
Angular emphasize to write reactive code with Observable/Promise - HTTP Requests, valueChanges
in FormControl, navigate
method in Router - even third-party libraries using this methodology, like @ngrx/store
, where you can access to state using selectors which are Observables.
Let's prepare example component where we want to display information from multiple sources.
@Component({
selector: 'app-pseudo-page',
standalone: true,
imports: [AsyncPipe],
templateUrl: './pseudo-page.component.html',
styleUrl: './pseudo-page.component.css',
})
export class PseudoPageComponent {
private readonly todoService = inject(TodoListService);
private currentPageSubject = new BehaviorSubject<number>(1);
readonly currentPage$ = this.currentPageSubject.asObservable();
private readonly currentUser$ = inject(CurrentUserService).currentUser$;
readonly loggedTime$ = interval(1000);
readonly todoList$ = this.currentPage$.pipe(
switchMap((page) => this.todoService.getTodos(page, 10))
);
prevPage() {
this.currentPageSubject.next(
this.currentPageSubject.value - 1 < 1
? 1
: this.currentPageSubject.value - 1
);
}
nextPage() {
this.currentPageSubject.next(
this.currentPageSubject.value + 1 > 10
? 10
: this.currentPageSubject.value + 1
);
}
To display them on the template we use Async
pipe - this one makes subscription by their own, will update the template when data will be changed and unsubscribe it when the component will be destroyed. We don't want to manage subscription manually.
Let's apply data in template. For this one, we use trick async + as XYZ
inside @if
.
<div class="pseudo-page__header">
<h3>Hello {{ currentUser$ | async }} !</h3>
<h3>You are logged for {{ (loggedTime$ | async) || 0 }} s</h3>
</div>
<div class="pseudo-page__container">
<div class="pseudo-page__todo-list">
<h2>There is your todo-list</h2>
<ul>
@for (element of (todoList$ | async); track element.id) {
<li>{{ element.title }}</li>
}
</ul>
<div class="pseudo-page__actions">
<button class="pseudo-page__button" (click)="prevPage();" [disabled]="((currentPage$ | async) || 1) <= 1">Prev</button>
<div>{{currentPage$ | async}}</div>
<button class="pseudo-page__button" (click)="nextPage();" [disabled]="((currentPage$ | async) || 1) >= 10">Next</button>
</div>
</div>
</div>
At first sight, the component code looks acceptable, but the template starts to look a little bit messy. We need to notice, even if async
is our friend in this case, each of them is a different subscription, which may cause huge issues with performance and code management.
To improve that, we can implement ViewModel pattern.
View-Model in Angular
View Model is a part of MVVM pattern (Model-View-ViewModel) and it's about seperate Model (M) from View (V) and create some kind of bridge to connect them - View Model (VM). In other words - ViewModel is an object, which (with Model's helps) decide which data will be passed into view.
Using Angular parts we can explain this Architecture like:
- Model is Component and all data what we have there
- View is Components's template
- View-Model is component property, which we will use inside template
Practical example
With this knowledge, let's try to refactor our code.
First - let's prepare variable, which we pass into our template. To keep the naming standard - let's call it vm$
. We use here functions combinateLatest
operator and map
. The first one will be used as combination of our all sources, the second to change value of each stream into one object.
readonly vm$ = combineLatest([
this.loggedTime$,
this.currentPage$,
this.currentUserService.currentUser$,
this.todoList$,
]).pipe(
map(([loggedTime, currentPage, currentUser, todoList]) => {
return {
loggedTime,
currentPage,
currentUser,
todoList
}
})
And that's all we need to do inside our component. And now we need to pass this variable to our template just like we already did, but instead of multiple async
pipe, we have only one, with all data which we're interested in. Because vm$
does refer to whole component's data - it's good to put at the top of the component template.
@if (vm$ | async; as vm) {
<div class="pseudo-page__header">
<h3>Hello {{ vm.currentUser }} !</h3>
<h3>You are logged for {{ vm.loggedTime }} s</h3>
</div>
<div class="pseudo-page__container">
<div class="pseudo-page__todo-list">
<h2>There is your todo-list</h2>
<ul>
@for (element of vm.todoList; track element.id) {
<li>{{ element.title }}</li>
}
</ul>
<div class="pseudo-page__actions">
<button class="pseudo-page__button" (click)="prevPage();" [disabled]="((vm.currentPage) || 1) <= 1">Prev</button>
<div>{{vm.currentPage }}</div>
<button class="pseudo-page__button" (click)="nextPage();" [disabled]="((vm.currentPage) || 1) >= 10">Next</button>
</div>
</div>
</div>
}
And that's it, from now on, the component will be updated every time when one of sources from combinateLatest
emit a new value. Even if you set OnPush
change detection strategy in this component, you will be almost sure that your component will be rerendered without calling detectChanges()
from ChangeDetectorRef
. Async
pipe is one of few solutions prepared from Angular team where when in changes your component is flagged for update.
This pattern learn you and your team how to separate things for component and for template. As you can see - the template know, which function from component it may call, it has access to some data - "some" data, not all of them. Additionally we can manipulate this streams values with other operators, like switchMap
, delay
, map
etc.
readonly vm$ = combineLatest([
// our source is "interval" but we want to display in template nice formated time m:ss
this.loggedTime$.pipe(
map((time) => {
const minutes = Math.floor(time / 60);
const seconds = time - minutes * 60;
return [minutes, ('0' + seconds).slice(-2)];
}),
map((time) => time.join(':')),
startWith(0)
),
this.currentPage$,
this.currentUserService.currentUser$,
this.todoList$.pipe(startWith([])),
]).pipe(
map(([loggedTime, currentPage, currentUser, todoList]) => {
return {
loggedTime,
currentPage,
currentUser,
todoList,
};
})
);
Hold on, I think I already saw it...
Yup, this pattern is recommended when you use @ngrx/store
and @ngrx/component-store
. Store is asynchronous, selectors returns Observable with data, so in case when you have to use multiple selectors from store in one component - using vm$
is recommended.
No Control-Flow in template? No problem!
You may ask - "How can I use it in my application? I still use the oldest version of Angular and I don't have control-flow in my templates..." and I'm happy to tell you that it is the pattern - It's not require any special libraries or specific Angular version , we can change tools to achieve our goals, so you can still use it!
As you already know, before control-flow (@if
, @for
etc) we use same "structural directives" like *ngIf
, *ngFor
etc. You can use them for creating subscription in your template (just like we already do with @if
) on <ng-container/>
or other element.
<ng-container *ngIf="(vm$ | async) as vm">
<!-- Access to your data as vm.SOMETHING -->
<div class="pseudo-page__header">
<h3>Hello {{ vm.currentUser }} !</h3>
<h3>You are logged for {{ vm.loggedTime }} s</h3>
</div>
...
</ng-container>
I feel obligated to mention that there are some prepared solutions dedicated mainly for managing asynchronous values/state in template like for ex. RxLet directive from @rx-angular/template package, which solve some issues and do improvements compare witth @if/*ngIf.
What's next? @let
instead of @if
Noteworthy is fact that Angular 18.1 will introduce the @let
syntax. This will allow you declare variables directly inside component template. Using new syntax, you can remove @if
block and create directly vm property
@let vm = (vm$ | async);
<!-- Access to your data as vm.SOMETHING -->
<div class="pseudo-page__header">
<h3>Hello {{ vm.currentUser }} !</h3>
<h3>You are logged for {{ vm.loggedTime }} s</h3>
</div>
Conclusion
The solution I just described above (@if
+ async
+ combinateLatest
) is probably the most popular and universal, but sometimes additional modification will be required.
combinateLatest
emit value when every sources emits their own. If one of them didn't emit value,combinateLatest
will wait for them. In order to fix it, you can try to usestartWith
operator on source.- Using
@if
to assign property fromasync
may cause some unexpected issues. If wholevm
will be "falsy value" (0, false, null etc) this directive will not render the component.
In this article I wanted to show you this technique. I use them every day and - for me - this is the base of my work to create "smart components".
Example code which I use in this article is available in StackBlitz, feel free to play with it!