@ngrx
component-store
Wojciech TrawiĆski
YouGov Front-end workshop, 2021
Introduction
Stand-alone library that helps to manage local/component state.
Alternative to reactive push-based "Service with a Subject".
Key concepts
- state has to be initialized (can be done lazily),
- state is tied to the life-cycle of a particular component and is cleaned up when that component is destroyed,
- state can be updated through the setState and patchState methods or updaters,
- state can be read through the select method or a top-level state$ observable,
- side-effects can be performed with the effect method.
Initialization
export interface MoviesState {
movies: Movie[];
}
@Injectable()
export class MoviesStore extends ComponentStore<MoviesState> {
constructor() {
super({movies: []});
}
}
Initialization
@Component({
template: `
<li *ngFor="let movie of (movies$ | async)">
{{ movie.name }}
</li>
`,
providers: [ComponentStore],
})
export class MoviesPageComponent {
readonly movies$ = this.componentStore.state$.pipe(
map(state => state.movies),
);
constructor(
private readonly componentStore: ComponentStore<{movies: Movie[]}>
) {}
}
Reading state
export interface MoviesState {
movies: Movie[];
}
@Injectable()
export class MoviesStore extends ComponentStore<MoviesState> {
constructor() {
super({movies:[]});
}
readonly movies$: Observable<Movie[]> = this.select(state => state.movies);
}
Reading state
@Component({
template: `
<li *ngFor="let movie of (movies$ | async)">
{{ movie.name }}
</li>
`,
providers: [MoviesStore],
})
export class MoviesPageComponent {
readonly movies$ = this.moviesStore.movies$;
constructor(private readonly moviesStore: MoviesStore) {}
}
Reading state
export interface MoviesState {
movies: Movie[];
userPreferredMoviesIds: string[];
}
@Injectable()
export class MoviesStore extends ComponentStore<MoviesState> {
constructor() {
super({movies:[], userPreferredMoviesIds:[]});
}
readonly movies$ = this.select(state => state.movies);
readonly userPreferredMovieIds$ = this.select(state => state.userPreferredMoviesIds);
readonly userPreferredMovies$ = this.select(
this.movies$,
this.userPreferredMovieIds$,
(movies, ids) => movies.filter(movie => ids.includes(movie.id))
);
}
Updating state
@Injectable()
export class MoviesStore extends ComponentStore<MoviesState> {
constructor() {
super({movies: []});
}
readonly addMovie = this.updater((state, movie: Movie) => ({
movies: [...state.movies, movie],
}));
}
Updating state
@Component({
template: `
<button (click)="add('New Movie')">Add a Movie</button>
`,
providers: [MoviesStore],
})
export class MoviesPageComponent {
constructor(private readonly moviesStore: MoviesStore) {}
add(movie: string) {
this.moviesStore.addMovie({ name: movie, id: generateId() });
}
}
Updating state
@Component({
template: `...`,
providers: [ComponentStore],
})
export class MoviesPageComponent implements OnInit {
constructor(
private readonly componentStore: ComponentStore<MoviesState>
) {}
resetMovies() {
this.componentStore.setState({movies: []});
}
addMovie(movie: Movie) {
this.componentStore.setState((state) => {
return {
...state,
movies: [...state.movies, movie],
};
});
}
}
Updating state
@Component({
template: `...`,
providers: [ComponentStore],
})
export class MoviesPageComponent implements OnInit {
constructor(
private readonly componentStore: ComponentStore<MoviesState>
) {}
updateSelectedMovie(selectedMovieId: string) {
this.componentStore.patchState({selectedMovieId});
}
addMovie(movie: Movie) {
this.componentStore.patchState((state) => ({
movies: [...state.movies, movie]
}));
}
}
Exercise #1
Reading state
private readonly fetchMoviesData$ = this.select(
this.store.select(getUserId),
moviesPerPage$,
currentPageIndex$,
(userId, moviesPerPage, currentPageIndex) => ({userId, moviesPerPage, currentPageIndex}),
);
Exercise #2
Side effects
@Injectable()
export class MoviesStore extends ComponentStore<MoviesState> {
readonly getMovie = this.effect((movieId$: Observable<string>) => {
return movieId$.pipe(
switchMap((id) => this.moviesService.fetchMovie(id).pipe(
tap({
next: (movie) => this.addMovie(movie),
error: (e) => this.logError(e),
}),
catchError(() => EMPTY),
)),
);
});
readonly addMovie = this.updater((state, movie: Movie) => ({
movies: [...state.movies, movie],
}));
}
Side effects
@Component({
template: `...`,
providers: [MoviesStore],
})
export class MovieComponent {
@Input()
set movieId(value: string) {
this.moviesStore.getMovie(value);
}
constructor(private readonly moviesStore: MoviesStore) {}
}
Side effects
@Injectable()
export class MoviesStore extends ComponentStore<MoviesState> {
constructor() {
super({movies: Movie[], moviesPerPage: 10, currentPageIndex: 0});
this.effect((moviePageData$) => {
return moviePageData$.pipe(
switchMap(({moviesPerPage, currentPageIndex}) =>
this.movieService.loadMovies(moviesPerPage, currentPageIndex),
).pipe(tap(results => this.updateMovieResults(results))),
);
})(this.fetchMoviesData$);
}
private readonly fetchMoviesData$ = this.select(
this.select(state => state.moviesPerPage),
this.select(state => state.currentPageIndex),
(moviesPerPage, currentPageIndex) => ({moviesPerPage, currentPageIndex}),
);
}
Exercise #3/4
@ngrx/component-store
By wojtrawi
@ngrx/component-store
- 317