Intro
Rxjs provides a number of mechanisms for caching HTTP request results. One good option is BehaviorSubject
, which emits a default value when first subscribed to (before the HTTP response is received), and will subsequently emit the latest result to all early and late subscribers. This is perfect for caching results in a service, and subscribing to that service from different components. Subscribing to a BehaviorSubject
combines the traditional steps of retrieving initial values, then listening for updates; a pattern commonly used to avoid timing issues with initial data retrieval delays
One of the variants of Subjects is the BehaviorSubject, which has a notion of "the current value". It stores the latest value emitted to its consumers, and whenever a new Observer subscribes, it will immediately receive the "current value" from the BehaviorSubject -- ReactiveX Manual
Route Resolvers
Resolvers in Angular provide a way of pre-fetching data before navigation to a route. When specifying a route
we set the resolve
property to an object with a key-value pair with a value that implements the @angular/router/Resolve
interface
{
path: 'data',
component: DataPageComponent,
resolve: { resolvedData: DataResolverService }
}
We choose the key name ourselves e.g. resolvedData, and reference this property when the component is loading
Route Resolve Interface
The resolver service we create must implement the resolve
function of the Resolve
interface. This function should return either a Promise, Observable, or value:T (of any Type). When returning an Observable, it must complete for the navigation to continue. In cases where we are working with a single observable, a straightforward map()
will usually suffice. If there are multiple data sources to resolve/pre-fetch, we need to do a little bit more
BehaviorSubject Observables
For this example we will create a mock data API service with two BehaviourSubject observables that we will subscribe to. The following is a class that replicates a service making 2 HTTP requests. We will use this as our API service from which we'll pre-fetch the data to be resolved before navigating to the route
data-api.service.ts
export class DataApiService implements AbstractDataApiService {
private clientsBehaviorSubject: BehaviorSubject<IClient[]> = new BehaviorSubject(undefined);
private accountsBehaviorSubject: BehaviorSubject<IAccount[]> = new BehaviorSubject(undefined);
private dataInitialized = false;
constructor() { }
getClients(): Observable<IClient[]> {
if (!this.dataInitialized) {
this.initData();
}
return this.clientsBehaviorSubject.asObservable();
}
getAccounts(): Observable<IAccount[]> {
if (!this.dataInitialized) {
this.initData();
}
return this.accountsBehaviorSubject.asObservable();
}
// This replicates XHR calls to the client and account API with a
// delayed response for each
private initData() {
this.dataInitialized = true;
timer(200)
.pipe(
map( value => {
const cl1: IClient = { id: 'Client 1'};
const cl2: IClient = { id: 'Client 2'};
return [cl1, cl2];
})
)
.subscribe( value => {
this.clientsBehaviorSubject.next(value);
});
timer(300)
.pipe(
map( value => {
const ac1: IAccount = { id: 'Account 1'};
const ac2: IAccount = { id: 'Account 2'};
return [ac1, ac2];
})
)
.subscribe( value => {
this.accountsBehaviorSubject.next(value);
});
}
}
CombineLatest
The resolver class, data-resolver.service.ts
will need to map the results of both BehaviorSubject observables from the data-api.service.ts
. Keeping in mind that a BehaviorSubject can return an initial default value (in our case undefined
) we will need to add a filter
that only maps the result when both BehaviorSubjects have emitted their latest 'truthy' values. Finally, after the filter
and map
, we call first()
, which completes the combineLatest
observable - and allows navigation to occur
combineLatest: This operator is best used when you have multiple, long-lived observables that rely on each other for some calculation or determination -- Learn Rxjs
data-resolver.service.ts
export interface IResolvedData {
clients: IClient[];
accounts: IAccount[];
}
@Injectable()
export class DataResolverService implements Resolve<IResolvedData> {
constructor(private dataServcie: AbstractDataApiService) {
}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<IResolvedData> {
const clients$ = this.dataServcie.getClients();
const accounts$ = this.dataServcie.getAccounts();
const combine$ = combineLatest(clients$, accounts$,
(clients, accounts) => {
return <IResolvedData>{ clients, accounts };
})
.pipe(
filter((value, index) => {
console.log('Resolver Filter: ', value);
return (value.clients && value.accounts) ? true : false;
}),
map(value => {
console.log('Resolver Map & Complete:', value);
return value;
}),
first()
);
return combine$;
}
}
StackBlitz Demo
Click on the Preview tab to see the route resolver in actions. The console panel at the bottom of the Perview tab displays the progress of the resolve()
function
⚡️StackBlitz is an incredible online editor based on VSCode. It can be used to run and edit Angular CLI generated GitHub repositories directly in the browser using the following url sequence stackblitz.com/github/{GH_USERNAME}/{REPO_NAME}
- really taking JavaScript editing on the web to the next level!