Wojciech TrawiĆski
YouGov Front-end workshop, 2021
Stand-alone library that helps to manage local/component state.
Alternative to reactive push-based "Service with a Subject".
export interface MoviesState {
movies: Movie[];
}
@Injectable()
export class MoviesStore extends ComponentStore<MoviesState> {
constructor() {
super({movies: []});
}
}
@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[]}>
) {}
}
export interface MoviesState {
movies: Movie[];
}
@Injectable()
export class MoviesStore extends ComponentStore<MoviesState> {
constructor() {
super({movies:[]});
}
readonly movies$: Observable<Movie[]> = this.select(state => state.movies);
}
@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) {}
}
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))
);
}
@Injectable()
export class MoviesStore extends ComponentStore<MoviesState> {
constructor() {
super({movies: []});
}
readonly addMovie = this.updater((state, movie: Movie) => ({
movies: [...state.movies, movie],
}));
}
@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() });
}
}
@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],
};
});
}
}
@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]
}));
}
}
private readonly fetchMoviesData$ = this.select(
this.store.select(getUserId),
moviesPerPage$,
currentPageIndex$,
(userId, moviesPerPage, currentPageIndex) => ({userId, moviesPerPage, currentPageIndex}),
);
@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],
}));
}
@Component({
template: `...`,
providers: [MoviesStore],
})
export class MovieComponent {
@Input()
set movieId(value: string) {
this.moviesStore.getMovie(value);
}
constructor(private readonly moviesStore: MoviesStore) {}
}
@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}),
);
}