Caching
Caching requests executed by Actions is a common practice. NGXS does not provide this ability out of the box, but it is easy to implement.
There are many different ways to approach this. Below is a simple example of using the store's current values and returning them instead of calling the HTTP service.
1
import { Injectable } from '@angular/core';
2
import { State, Action, StateContext } from '@ngxs/store';
3
import { tap } from 'rxjs/operators';
4
5
export class GetNovels {
6
static readonly type = '[Novels] Get novels';
7
}
8
9
@State<Novel[]>({
10
name: 'novels',
11
defaults: []
12
})
13
@Injectable()
14
export class NovelsState {
15
constructor(private novelsService: NovelsService) {}
16
17
@Action(GetNovels)
18
getNovels(ctx: StateContext<Novel[]>) {
19
return this.novelsService.getNovels().pipe(tap(novels => ctx.setState(novels)));
20
}
21
}
Copied!
Imagine that this state of novels contains only minimal information about them such as ID and name. When the user selects a particular novel - he is redirected to a page with full information about this novel. We want to load this information only once. Let's create a state and call it novelsInfo, this will be the object whose keys are the identifiers of the novels:
1
import { Injectable } from '@angular/core';
2
import { State, Action, StateContext, createSelector } from '@ngxs/store';
3
import { tap } from 'rxjs/operators';
4
5
export interface NovelsInfoStateModel {
6
}
7
8
export class GetNovelById {
9
static readonly type = '[Novels info] Get novel by ID';
10
constructor(public id: string) {}
11
}
12
13
@State<NovelsInfoStateModel>({
14
name: 'novelsInfo',
15
defaults: {}
16
})
17
@Injectable()
18
export class NovelsInfoState {
19
static getNovelById(id: string) {
20
return createSelector([NovelsInfoState], (state: NovelsInfoStateModel) => state[id]);
21
}
22
23
constructor(private novelsService: NovelsService) {}
24
25
@Action(GetNovelById)
26
getNovelById(ctx: StateContext<NovelsInfoStateModel>, action: GetNovelById) {
27
const novels = ctx.getState();
28
const id = action.id;
29
30
if (novels[id]) {
31
// If the novel with ID has been already loaded
32
// we just break the execution
33
return;
34
}
35
36
return this.novelsService.getNovelById(id).pipe(
37
tap(novel => {
38
ctx.patchState({ [id]: novel });
39
})
40
);
41
}
42
}
Copied!
The component, that displays information about the novel, can subscribe to the params observable of the ActivatedRoute to listen to the params change. The code will look as following:
1
@Component({
2
selector: 'app-novel',
3
template: `
4
<h1>{{ novel?.title }}</h1>
5
<span>{{ novel?.author }}</span>
6
<p>
7
{{ novel?.content }}
8
<del datetime="{{ novel?.publishedAt }}"></del>
9
</p>
10
`
11
})
12
export class NovelComponent implements OnDestroy {
13
novel: Novel;
14
15
private destroy$ = new Subject<void>();
16
17
constructor(route: ActivatedRoute, store: Store) {
18
route.params
19
.pipe(
20
switchMap(params =>
21
store
22
.dispatch(new GetNovelById(params.id))
23
.pipe(mergeMap(() => store.select(NovelsInfoState.getNovelById(params.id))))
24
),
25
takeUntil(this.destroy$)
26
)
27
.subscribe(novel => {
28
this.novel = novel;
29
});
30
}
31
32
ngOnDestroy() {
33
this.destroy$.next();
34
this.destroy$.complete();
35
}
36
}
Copied!
We're using the switchMap in this example, so if the user navigates to another novel and params observable emits new value - we have to complete previously started asynchronous job (in our case it's getting novel by ID).
Last modified 1yr ago
Copy link