Angular state management with RxCache: Part 2
To subscribe or not to subscribe, that is the question.
In this follow up to part 1 I will discuss the difference between the Observable properties and direct access properties to the data stored in an RxCache item. If you haven't read part 1 then you should catch up on it first as I will skip over what we covered in that article. Click here to read part 1.
An RxCache item has multiple properties that gives us information about the state stored in it, value$, clone$, loading$, loaded$, saving$, saved$, deleting$, deleted$, hasError$ and error$. These observables can be subscribed to either in TypeScript code or in the template with the async pipe so our application can get notified when a value is emitted down the pipe. But all the information is also exposed with non observable properties as well, every property has a corresponding value, clone, loading, loaded, saving, saved, deleting, deleted, hasError and error property.
With all the magic of RxJs and push based data flow why would we want to not use the observable properties but instead directly access the data? Because sometimes it is convenient when we don’t need to listen for changes on that data.
In this part we will extend the application, https://stackblitz.com/edit/angular-byjpz2 to have an orders component where we show the orders for a user.
First we will go into the user service and change the user$ property from
user$ = this.userCache.clone$;
to
user$ = this.userCache.value$;
and add a non observable property as well
get user() {
return this.userCache.value;
}
We are no longer using the clone object because in a few places we will be using the user$ property on the service for informational purposes that do not mutate the object. It is rather inefficient to be cloning the object every time we want to access a property.
Now that we are not cloning the object in the service we will have to do it in the component. The user component from part one has been renamed to user details and if you take a look at the template we have cloned the output from the async pipe with the clone pipe.
<form *ngIf="user$ | async | clone as user" (submit)="save(user)">
The clone pipe is exported by the RxCacheModule so we need to add it to our app module.
import { RxCacheModule } from 'ngx-rxcache';
and
imports: [ BrowserModule, FormsModule, AppRoutingModule, RxCacheModule ],
Another option to cloning is doing it with a map in TypeScript
import { clone } from 'ngx-rxcache'
and
user$ = this.userService.user$.pipe(map(user => clone(user)));
Either way achieves the same result, we are creating a copy of the object so that we can use template forms that two way bind to the data. Two way binding mutates the data and we want to avoid mutating the data in the cache. You could also use reactive forms rather than cloning but I find template forms to be much more convenient.
If you look in the user details component you will see that it is nearly identical to the old user component but it does not call the load method on the service. So where is the call to load? There is a new user component that is a router outlet that hosts all components that are related to user data.
<app-spinner *ngIf="loading$ | async else loaded">loading</app-spinner>
<ng-template #loaded>
<ul *ngIf="user$ | async as user">
<li>{{user.firstName}} {{user.lastName}}</li>
<li>
<a [routerLink]="'/user/' + user.id + '/details'">Details</a>
</li>
<li>
<a [routerLink]="'/user/' + user.id + '/orders'">Orders</a>
</li>
<li><a routerLink="/users">Users</a></li>
</ul>
<router-outlet></router-outlet>
</ng-template>
And it is in this component where we load the user data.
import { Component, OnDestroy } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subject } from 'rxjs';
import { map, takeUntil } from 'rxjs/operators';import { UserService } from '../../services/user.service';@Component({
selector: 'app-user',
templateUrl: './user.component.html',
styleUrls: ['./user.component.css']
})
export class UserComponent implements OnDestroy {
loading$ = this.userService.loading$; user$ = this.userService.user$; finalise = new Subject<void>(); constructor(
private userService: UserService,
route: ActivatedRoute
) {
route.params.pipe(
map(params => params.id),
takeUntil(this.finalise)
).subscribe(id => {
userService.load(parseInt(id));
});
} ngOnDestroy() {
this.finalise.next();
this.finalise.complete();
this.userService.reset();
}}
Now we have a user menu with links to user details and orders. Both of these components are hosted under the user component. Because the user component shows a loading spinner that hides the router outlet when the user data is loading, we can be sure that the user data is already loaded when we are inside one of our components that are hosted under this router outlet as the cache item will only emit a false down the loading$ pipe once the data is in place. This will cause the router outlet to display and the components will render after the data is available.
We host our user details and orders components with routes as children of the user component
{
path: 'user/:id',
component: UserComponent,
children: [
{ path: 'details', component: UserDetailsComponent },
{ path: 'orders', component: OrdersComponent }
]
}
If we take a look inside the orders component
import { Component, OnInit } from '@angular/core';import { OrdersService } from '../../services/orders.service';
import { UserService } from '../../services/user.service';@Component({
selector: 'app-orders',
templateUrl: './orders.component.html',
styleUrls: ['./orders.component.css']
})
export class OrdersComponent {
orders$ = this.ordersService.orders$; loading$ = this.ordersService.loading$; constructor(
private ordersService: OrdersService,
userService: UserService
) {
ordersService.load(userService.user.id);
}
}
We can use the user property on the user service to directly access the id and pass it to the orders service with the sound knowledge that the data is already there as the component wont be rendered until the router outlet is there. No complex selectors, subscriptions, combine latest or switch maps required, just a simple old property to access the data.
A complete and working example can be viewed on StackBlitz https://stackblitz.com/edit/angular-byjpz2