Creating your own store for local state
Before we learn how to use @ngrx/component-store
itself, we are first going to look at an example of using a standard service that is provided directly to a component as a way to store state information.
This is the technique that is generally used with Component Store, so it will be useful to use as a baseline to compare what Component Store actually provides for us. Then, when we do add @ngrx/component-store
in the next lesson, we will already have part of the work completed.
We are going to take a smart component from one of the applications we have built already and refactor it to use this service approach.
Refactoring to store state in a service
A great example to use for a refactor is the HomeComponent
from the Giflist application. If you have already completed this application yourself you can follow along with this refactor. If you don’t already have the code for the Giflist application you can go back to that module and just clone the source code directly.
For reference, let’s just take a look at all of the state we are managing inside of this HomeComponent
:
currentlyLoadingGifs$ = new BehaviorSubject<string[]>([]);
loadedGifs$ = new BehaviorSubject<string[]>([]);
settingsModalIsOpen$ = new BehaviorSubject<boolean>(false);
subredditFormControl = new FormControl('gifs');
// Combine the stream of gifs with the streams determining their loading status
gifs$ = combineLatest([
this.redditService.getGifs(this.subredditFormControl),
this.currentlyLoadingGifs$,
this.loadedGifs$,
]).pipe(
map(([gifs, currentlyLoadingGifs, loadedGifs]) =>
gifs.map((gif) => ({
...gif,
loading: currentlyLoadingGifs.includes(gif.permalink),
dataLoaded: loadedGifs.includes(gif.permalink),
}))
)
);
vm$ = combineLatest([
this.gifs$.pipe(startWith([])),
this.settingsService.settings$,
this.redditService.isLoading$,
this.settingsModalIsOpen$,
]).pipe(
map(([gifs, settings, isLoading, modalIsOpen]) => ({
gifs,
settings,
isLoading,
modalIsOpen,
}))
);
It’s not anything too overwhelming - I would say this component is still within a reasonable/manageable amount of complexity. However, when I look at this component, it definitely feels a little messy.
A good indicator for me as to whether a component is doing too much is if you can quickly understand the code by glancing at it. With all of this extra state information in the component, it is going to take a little while longer to understand what is going on. If all of the state information is separated into a service dedicated to managing state, the role of the component and the state service become a lot more clear/obvious.
Let’s get started with the refactor.
Create a file a
src/app/home/data-access/home.store.ts
and add the following:
import { Injectable } from '@angular/core';
import { FormControl } from '@angular/forms';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { RedditService } from '../../shared/data-access/reddit/reddit.service';
@Injectable()
export class HomeStore {
currentlyLoadingGifs$ = new BehaviorSubject<string[]>([]);
loadedGifs$ = new BehaviorSubject<string[]>([]);
settingsModalIsOpen$ = new BehaviorSubject<boolean>(false);
subredditFormControl = new FormControl('gifs');
// Combine the stream of gifs with the streams determining their loading status
gifs$ = combineLatest([
this.redditService.getGifs(this.subredditFormControl),
this.currentlyLoadingGifs$,
this.loadedGifs$,
]).pipe(
map(([gifs, currentlyLoadingGifs, loadedGifs]) =>
gifs.map((gif) => ({
...gif,
loading: currentlyLoadingGifs.includes(gif.permalink),
dataLoaded: loadedGifs.includes(gif.permalink),
}))
)
);
constructor(private redditService: RedditService) {}
setLoading(permalink: string) {
// Add the gifs permalink to the loading array
this.currentlyLoadingGifs$.next([
...this.currentlyLoadingGifs$.value,
permalink,
]);
}
setLoadingComplete(permalinkToComplete: string) {
this.loadedGifs$.next([...this.loadedGifs$.value, permalinkToComplete]);
this.currentlyLoadingGifs$.next([
...this.currentlyLoadingGifs$.value.filter(
(permalink) => !this.loadedGifs$.value.includes(permalink)
),
]);
}
}
This refactor is actually reasonably easy - we have pretty much just copy and pasted anything related to managing the state for this component into the store service. That includes all of the class member variables related to managing state, as well as the two methods being used to update that state information.
NOTE: Notice that unlike most services we create, this @Injectable
does not make use of the providedIn
property. That is because we will be providing it manually.
Delete all the code we just added to the store service from
HomeComponent
Now we need to update our HomeComponent
to make use of the new HomeStore
service.
Add the
HomeStore
provider directly to theHomeComponent
metadata:
@Component({
selector: 'app-home',
// ...snip
providers: [HomeStore],
changeDetection: ChangeDetectionStrategy.OnPush,
})
We usually use providedIn: root
to make a service globally accessible within out application. In this case, we are providing it directly to the HomeComponent
because it is the only component that needs to use it. Technically, we could also just use providedIn: root
and only inject HomeStore
into the HomeComponent
, but providing it directly to the component is a little safer. This way, we know no other parts of the application can effect the state in the store for this component (unless the HomeStore
explicitly uses state from somewhere else in the application).
Inject the
HomeStore
in theHomeComponent
:
constructor(
public store: HomeStore,
public redditService: RedditService,
private settingsService: SettingsService
) {}
Modify the
vm$
to use the store:
vm$ = combineLatest([
this.store.gifs$.pipe(startWith([])),
this.settingsService.settings$,
this.redditService.isLoading$,
this.store.settingsModalIsOpen$,
]).pipe(
map(([gifs, settings, isLoading, modalIsOpen]) => ({
gifs,
settings,
isLoading,
modalIsOpen,
}))
);
Modify the template where necessary to use methods from the store
Now, our HomeComponent
is a lot cleaner with all of the state management code abstracted away into its own service:
export class HomeComponent {
vm$ = combineLatest([
this.store.gifs$.pipe(startWith([])),
this.settingsService.settings$,
this.redditService.isLoading$,
this.store.settingsModalIsOpen$,
]).pipe(
map(([gifs, settings, isLoading, modalIsOpen]) => ({
gifs,
settings,
isLoading,
modalIsOpen,
}))
);
constructor(
public store: HomeStore,
public redditService: RedditService,
private settingsService: SettingsService
) {}
loadMore(ev: Event, currentGifs: Gif[]) {
this.redditService.nextPage(ev, currentGifs[currentGifs.length - 1].name);
}
}
This is the sort of code that looks less chaotic and we can more easily quickly tell what is going on.
Now that we have seen what abstracting some state into a service looks like, in the next lesson we will see what it would look like by bringing @ngrx/component-store
into the mix.