Angular state management with RxCache
What is RxCache?
RxCache is a light weight state management library for Angular built on RxJs behaviour subjects. It is used to implement a single source of truth application state with barley any boilerplate code. It is designed to help you write efficient, easy to maintain and testable services.
The source code for RxCache can be viewed on GitHub at https://github.com/adriandavidbrand/ngx-rxcache and there are a few demo applications on StackBlitz.
StackBlitz testbed: https://stackblitz.com/edit/angular-3yqpfe
The official ngrx demo app redone in RxCache: https://stackblitz.com/edit/github-tsrf1f
The subject of this article is a CRUD demo app of a simple user management interface: https://stackblitz.com/edit/angular-jxqaiv
We start with RxCache by adding a cache item to one of our services. We add the RxCacheService to the dependency list in our services constructor with
constructor(private cache: RxCacheService)
and then we can add a cacheItem as a property to the service with
private usersCache = this.cache.get<User[]>({ id: '[Users] users' });
This is the simplest cache item we can add where the configuration only has an id that is a unique string to identify the cache item stored in the service. The string can be any unique string but the convention usually used is ‘[ServiceName] dataStored’ the name of the service in square brackets followed by the data stored in the cache item.
This is not any more useful than a behaviour subject would be in this state as all we have is a cache item that can store a list of users.
To be more interesting we can add a constructor function that returns an observable of the cache item’s type that can be used to populate the cache.
private usersCache = this.cache.get<User[]>({
id: '[Users] users',
construct: () => this.http.get<User[]>('user')
});
Now if we call the load method on the cache item the constructor function will be called to populate the cache item.
We can then add some extra properties to the service to expose information about what is in the cache.
users$ = this.usersCache.value$;
exposes an observable to subscribe to the contents of the cache item and
loading$ = this.usersCache.loading$;
exposes an observable to show when the cache item is loading.
This is all we need for our Users service.
import { Injectable } from '@angular/core';
import { RxCacheService } from 'ngx-rxcache';import { MockHttpService } from './mock-http.service';
import { User } from '../models/user';@Injectable({
providedIn: 'root'
})
export class UsersService {
private usersCache = this.cache.get<User[]>({
id: '[Users] users',
construct: () => this.http.get<User[]>('user')
}); users$ = this.usersCache.value$;
loading$ = this.usersCache.loading$; constructor(
private http: MockHttpService,
private cache: RxCacheService
) {} load() {
this.usersCache.load();
}
}
The MockHttpService is to simulate some http requests to a rest api, in a real application we would be using the Angular HttpClient to make real http requests.
We can now create a Users component to display the data from the users service.
The TypeScript for the Users component is very simple, we just add the UsersService to the dependency list in the constructor and expose the users$ and loading$ observables as properties. We also call the load method in the constructor to refresh the data in the cache with the cache items constructor function.
import { Component } from '@angular/core';import { UsersService } from '../../services/users.service';@Component({
selector: 'app-users',
templateUrl: './users.component.html',
styleUrls: ['./users.component.css']
})
export class UsersComponent {
users$ = this.usersService.users$;
loading$ = this.usersService.loading$; constructor(private usersService: UsersService) {
usersService.load();
}
}
The markup in the html is also simple. Rather than subscribing to observables in the TypeScript of the component, we use the async pipe in the markup to manage the subscriptions for us. First the loading$ observable is used to show a spinner component while the data is being retrieved from the server and then the loaded template is shown. We use the async pipe to get the data from the users$ observable and assign it to a view variable called users. We then use ngFor to bind the users view variable to some rows in the table.
<h1>Users</h1><app-spinner *ngIf="loading$ | async else loaded">loading</app-spinner>
<ng-template #loaded>
<table *ngIf="users$ | async as users">
<thead>
<tr *ngIf="users.length">
<th>Name</th>
<th>Email</th>
</tr>
<tr *ngIf="users.length === 0">
<th>No users</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of users">
<td>{{user.firstName}} {{user.lastName}}</td>
<td>{{user.email}}</td>
</tr>
</tbody>
</table>
</ng-template>
We now have a working application that can retrieve a list of users from the api and display them in a table.
The next step is to add a user component to our application for editing existing and adding new users.
Our user component has a title field on the form that is a dropdown list populated with a list of titles retrieved from the server. Before we start with the user component we will create a RefDataService that will hold reference data.
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { RxCacheService } from 'ngx-rxcache';import { MockHttpService } from './mock-http.service';
import { SelectItem } from '../models/select-item';@Injectable({
providedIn: 'root'
})
export class RefDataService {
private titlesCache = this.cache.get<SelectItem[]>({
id: '[RefData] titles',
construct: () => this.http.get<SelectItem[]>('titles'),
autoload: true
}); constructor(
private http: MockHttpService,
private cache: RxCacheService
) {} get titles$(): Observable<SelectItem[]> {
return this.titlesCache.value$;
}
}
For each reference data item we would hold in the service such as titles, states, gender etc we have a cache item the same as in the users service but with the addition of the autoload property set to true. Autoload means that the cache item will automatically trigger the constructor function the first time the contents of the cache are accessed with either the asynchronous property value$ or the value property.
The titles$ are then exposed as a getter property, the getter part is important so that the value$ property of the cache item is only accessed when the data is needed which will trigger the auto load. If we exposed the value$ property directly as we did in the users service
titles$ = this.titlesCache.value$;
the autoload would kick in and fire off a http request instantly. Using a getter property like this a http request is not fired until the first time the getter property is accessed.
We can have RefDataService with as many reference data cache items as we like and using getter properties like this means that a http request will only be fired the first time the data is needed and then it will be cached for all subsequent times that data is accessed.
Our user component will also need a user service. Our user cache item we create has a few extra properties for save, delete and error handling functions.
The save function, checks to see if the user has an id and http puts the existing user if so otherwise it posts the new user. The delete function does a http delete.
The errorHandler can be used to log error, display modals or handle errors that may occur in any way necessary the application requires. If the errorHandler function returns a string then that string will populate the error$ observable of the cache item.
We then expose observables for the user, loading, saving and the error. Note that we have used the clone$ property of the cache item to expose the user rather than the value$ property. The clone is a deep clone of the data stored in the cache and can be mutated without effecting the data stored in the cache. This causes any subscriptions to the observable to get a clone of the user rather than the instance that is in the cache. This is important because in the component we will be using template forms to two way bind to a view variable we assign to the async pipe’s subscription. Two way binding with template forms mutates that data and we do not want to mutate the data that is stored in the cache so binding to a clone preserves the integrity of the cache’s data. The only way we should every update data in the cache is by passing in a new instance of the data object with the update method or by calling load and have the constructor function update the data.
import { Injectable } from '@angular/core';
import { RxCacheService } from 'ngx-rxcache';import { MockHttpService } from './mock-http.service';
import { User } from '../models/user';@Injectable({
providedIn: 'root'
})
export class UserService {
private userCache = this.cache.get<User>({
id: '[User] user',
save: (user: User) => user.id ? this.http.put<User>(`user/${user.id}`, user) : this.http.post<User>('user', user),
delete: (user: User) => this.http.delete(`user/${user.id}`),
errorHandler: (response) => response.error
}); user$ = this.userCache.clone$;
loading$ = this.userCache.loading$;
saving$ = this.userCache.saving$;
error$ = this.userCache.error$; constructor(
private http: MockHttpService,
private cache: RxCacheService
) {} save(user: User, saved?: (response) => void) {
this.userCache.save(user, saved);
} delete(user: User, deleted?: (response) => void) {
this.userCache.delete(user, deleted);
} load(id?: number) {
if (id) {
this.userCache.load(() => this.http.get<User>(`user/${id}`));
} else {
this.userCache.update({
id: null,
title: null,
firstName: '',
lastName: '',
email: ''
});
}
}
}
Now we can create our user component and use the RefDataService to expose the titles and the user service to expose the properties and save function for the user. We use the route to get the user id from the url to pass it to the load function. In our service if that id is undefined it will create a new user or load one if it is an id.
import { Component } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { map } from 'rxjs/operators';import { UserService } from '../../services/user.service';
import { RefDataService } from '../../services/ref-data.service';
import { User } from '../../models/user';@Component({
selector: 'app-user',
templateUrl: './user.component.html',
styleUrls: ['./user.component.css']
})
export class UserComponent {
user$ = this.userService.user$;
loading$ = this.userService.loading$;
saving$ = this.userService.saving$;
error$ = this.userService.error$;
titles$ = this.refDataService.titles$; constructor(
private userService: UserService,
private refDataService: RefDataService,
private route: ActivatedRoute,
private router: Router
) {
const id = route.snapshot.paramMap.get('id');
userService.load(parseInt(id));
} save(user: User) {
this.userService.save(user, _ => {
this.router.navigate(['/users']);
});
}
}
Now in the template we can use template forms to two way bind to the clone of the user object.
<h1>User</h1><app-spinner *ngIf="loading$ | async else loaded">loading</app-spinner><ng-template #loaded> <div class="error" *ngIf="error$ | async as error">{{error}}</div> <form *ngIf="user$ | async as user" (submit)="save(user)"> <label for="title">Title</label><br>
<select id="title" name="title" [(ngModel)]="user.title">
<option [ngValue]="null">Please select</option>
<option *ngFor="let title of titles$ | async" [value]="title.value">{{title.label}}</option>
</select><br> <label for="firstName">First Name</label><br>
<input type="text" id="firstName" name="firstName" [(ngModel)]="user.firstName"><br> <label for="lastName">Last Name</label><br>
<input type="text" id="lastName" name="lastName" [(ngModel)]="user.lastName"><br> <label for="email">Email</label><br>
<input type="text" id="email" name="email" [(ngModel)]="user.email"><br> <app-spinner *ngIf="saving$ | async else saved">saving</app-spinner>
<ng-template #saved>
<button>Save</button>
</ng-template>
</form></ng-template><a routerLink="/users">Cancel</a>
Normally we would have some client side form validation but here we are showing how we can validate server side.
We then add two routes to the component, one with an id parameter for editing an existing user and one without to create a new user.
{ path: 'user', component: UserComponent },
{ path: 'user/:id', component: UserComponent }
Then we can change the users name in our table to be a link to edit the user.
<td><a [routerLink]="'/user/' + user.id">{{user.firstName}} {{user.lastName}}</a></td>
And we can also add a link under the table to add a new user.
<a routerLink="../user">Add new user</a>
We can also add a delete link in the table to delete a user.
<td><button (click)="delete(user)">delete</button></td>
We then need to add the UserService to the dependency list in the constructor of the UsersComponent
constructor(private usersService: UsersService, private userService: UserService) {
and then add a delete function
delete(user: User) {
this.userService.delete(user, _ => {
this.usersService.load();
});
}
that passes in the selected user and a callback function that reloads the users after the delete has finished.
This is a quick introduction to using RxCache and how it can be used to help you write simple, easy to understand and maintain Angular services.
A complete and working example can be view on StackBlitz at https://stackblitz.com/edit/angular-jxqaiv
In part 2 we go over accessing data stored in the cache using observables vs direct access.