Upgrade to Pro — share decks privately, control downloads, hide ads and more …

NgRx - Core Principles & New Features

NgRx - Core Principles & New Features

Marko Stanimirović

February 14, 2023
Tweet

More Decks by Marko Stanimirović

Other Decks in Programming

Transcript

  1. Marko Stanimirović @MarkoStDev ★ Sr. Frontend Engineer at JobCloud ★

    NgRx Core Team Member ★ Google Developer Expert in Angular ★ Angular Belgrade Organizer ★ M.Sc. in Software Engineering
  2. musicians-page.actions.ts import { createAction } from '@ngrx/store'; export const opened

    = createAction( '[Musicians Page] Opened' ); source event
  3. musicians-page.actions.ts import { createAction, props } from '@ngrx/store'; export const

    opened = createAction( '[Musicians Page] Opened' ); export const queryChanged = createAction( '[Musicians Page] Query Changed', props<{ query: string }>() );
  4. musicians-api.actions.ts import { createAction, props } from '@ngrx/store'; export const

    musiciansLoadedSuccess = createAction( '[Musicians API] Musicians Loaded Success', props<{ musicians: Musician[] }>() );
  5. musicians-api.actions.ts import { createAction, props } from '@ngrx/store'; export const

    musiciansLoadedSuccess = createAction( '[Musicians API] Musicians Loaded Success', props<{ musicians: Musician[] }>() ); export const musiciansLoadedFailure = createAction( '[Musicians API] Musicians Loaded Failure', props<{ message: string }>() );
  6. musicians.reducer.ts export interface State { musicians: Musician[]; isLoading: boolean; query:

    string; } const initialState: State = { musicians: [], isLoading: false, query: '', };
  7. musicians.reducer.ts import { createReducer } from '@ngrx/store'; export interface State

    { musicians: Musician[]; isLoading: boolean; query: string; } const initialState: State = { musicians: [], isLoading: false, query: '', }; export const reducer = createReducer( );
  8. musicians.reducer.ts import { createReducer } from '@ngrx/store'; export interface State

    { musicians: Musician[]; isLoading: boolean; query: string; } const initialState: State = { musicians: [], isLoading: false, query: '', }; export const reducer = createReducer( initialState, );
  9. musicians.reducer.ts import { createReducer, on } from '@ngrx/store'; import *

    as musiciansPageActions from './musicians-page.actions'; export interface State { musicians: Musician[]; isLoading: boolean; query: string; } const initialState: State = { musicians: [], isLoading: false, query: '', }; export const reducer = createReducer( initialState, on( musiciansPageActions.opened, (state) => ({ ==.state, isLoading: true }) ), );
  10. musicians.reducer.ts import { createReducer, on } from '@ngrx/store'; import *

    as musiciansPageActions from './musicians-page.actions'; export interface State { musicians: Musician[]; isLoading: boolean; query: string; } const initialState: State = { musicians: [], isLoading: false, query: '', }; export const reducer = createReducer( initialState, on( musiciansPageActions.opened, (state) => ({ ==.state, isLoading: true }) ), ); immutable state update
  11. musicians.reducer.ts import { createReducer, on } from '@ngrx/store'; import *

    as musiciansPageActions from './musicians-page.actions'; import * as musiciansApiActions from './musicians-api.actions'; export interface State { musicians: Musician[]; isLoading: boolean; query: string; } const initialState: State = { musicians: [], isLoading: false, query: '', }; export const reducer = createReducer( initialState, on( musiciansPageActions.opened, (state) => ({ ==.state, isLoading: true }) ), on( musiciansPageActions.queryChanged, (state, { query }) => ({ ==.state, query }) ), on( musiciansApiActions.musiciansLoadedSuccess, (state, { musicians }) => ({ ==.state, musicians, isLoading: false }) ) );
  12. main.ts import { bootstrapApplication } from '@angular/platform-browser'; import { provideStore

    } from '@ngrx/store'; import * as fromMusicians from './app/musicians/musicians.reducer'; bootstrapApplication(AppComponent, { providers: [ provideStore({ musicians: fromMusicians.reducer }), ], }); Providing Store and Registering Reducers
  13. main.ts import { bootstrapApplication } from '@angular/platform-browser'; import { provideStore,

    provideState } from '@ngrx/store'; import * as fromMusicians from './app/musicians/musicians.reducer'; bootstrapApplication(AppComponent, { providers: [ provideStore(), provideState('musicians', fromMusicians.reducer), ], }); Providing Store and Registering Reducers
  14. musicians.selectors.ts import { createFeatureSelector } from '@ngrx/store'; import * as

    fromMusicians from './musicians.reducer'; export const selectMusiciansState = createFeatureSelector<fromMusicians.State>('musicians');
  15. musicians.selectors.ts import { createFeatureSelector, createSelector } from '@ngrx/store'; import *

    as fromMusicians from './musicians.reducer'; export const selectMusiciansState = createFeatureSelector<fromMusicians.State>('musicians'); export const selectMusicians = createSelector( selectMusiciansState, (state) => state.musicians );
  16. musicians.selectors.ts import { createFeatureSelector, createSelector } from '@ngrx/store'; import *

    as fromMusicians from './musicians.reducer'; export const selectMusiciansState = createFeatureSelector<fromMusicians.State>('musicians'); export const selectMusicians = createSelector( selectMusiciansState, (state) => state.musicians ); export const selectIsLoading = createSelector( selectMusiciansState, (state) => state.isLoading ); export const selectQuery = createSelector( selectMusiciansState, (state) => state.query );
  17. musicians.selectors.ts import { createFeatureSelector, createSelector } from '@ngrx/store'; import *

    as fromMusicians from './musicians.reducer'; export const selectMusiciansState = createFeatureSelector<fromMusicians.State>('musicians'); export const selectMusicians = createSelector( selectMusiciansState, (state) => state.musicians ); export const selectIsLoading = createSelector( selectMusiciansState, (state) => state.isLoading ); export const selectQuery = createSelector( selectMusiciansState, (state) => state.query ); export const selectFilteredMusicians = createSelector( selectMusicians, selectQuery, (musicians, query) => musicians.filter(({ name }) => name.toLowerCase().includes(query)) );
  18. musicians.component.ts import { Store } from '@ngrx/store'; @Component({ selector: 'app-musicians',

    changeDetection: ChangeDetectionStrategy.OnPush, }) export class MusiciansComponent implements OnInit { private readonly store = inject(Store); }
  19. musicians.component.ts import { Store } from '@ngrx/store'; import * as

    musiciansPageActions from './musicians-page.actions'; @Component({ selector: 'app-musicians', changeDetection: ChangeDetectionStrategy.OnPush, }) export class MusiciansComponent implements OnInit { private readonly store = inject(Store); ngOnInit(): void { this.store.dispatch(musiciansPageActions.opened()); } }
  20. musicians.component.ts import { Store } from '@ngrx/store'; import * as

    musiciansPageActions from './musicians-page.actions'; @Component({ selector: 'app-musicians', changeDetection: ChangeDetectionStrategy.OnPush, }) export class MusiciansComponent implements OnInit { private readonly store = inject(Store); ngOnInit(): void { this.store.dispatch(musiciansPageActions.opened()); } onSearch(query: string): void { this.store.dispatch( musiciansPageActions.queryChanged({ query }) ); } }
  21. musicians.component.ts import { Store } from '@ngrx/store'; import * as

    musiciansPageActions from './musicians-page.actions'; import { selectFilteredMusicians } from './musicians.selectors'; @Component({ selector: 'app-musicians', changeDetection: ChangeDetectionStrategy.OnPush, }) export class MusiciansComponent implements OnInit { private readonly store = inject(Store); readonly musicians$ = this.store.select(selectFilteredMusicians); ngOnInit(): void { this.store.dispatch(musiciansPageActions.opened()); } onSearch(query: string): void { this.store.dispatch( musiciansPageActions.queryChanged({ query }) ); } }
  22. musicians.component.ts import { Store } from '@ngrx/store'; import * as

    musiciansPageActions from './musicians-page.actions'; import { selectFilteredMusicians, selectIsLoading, selectQuery, } from './musicians.selectors'; @Component({ selector: 'app-musicians', changeDetection: ChangeDetectionStrategy.OnPush, }) export class MusiciansComponent implements OnInit { private readonly store = inject(Store); readonly musicians$ = this.store.select(selectFilteredMusicians); readonly isLoading$ = this.store.select(selectIsLoading); readonly query$ = this.store.select(selectQuery); ngOnInit(): void { this.store.dispatch(musiciansPageActions.opened()); } onSearch(query: string): void { this.store.dispatch( musiciansPageActions.queryChanged({ query }) ); } }
  23. musicians.component.ts musicians.component.html import { Store } from '@ngrx/store'; import *

    as musiciansPageActions from './musicians-page.actions'; import { selectFilteredMusicians, selectIsLoading, selectQuery, } from './musicians.selectors'; @Component({ selector: 'app-musicians', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [AsyncPipe, SearchBoxComponent, MusicianListComponent], templateUrl: './musicians.component.html', }) export class MusiciansComponent implements OnInit { private readonly store = inject(Store); readonly musicians$ = this.store.select(selectFilteredMusicians); readonly isLoading$ = this.store.select(selectIsLoading); readonly query$ = this.store.select(selectQuery); ngOnInit(): void { this.store.dispatch(musiciansPageActions.opened()); } onSearch(query: string): void { this.store.dispatch( musiciansPageActions.queryChanged({ query }) ); } }
  24. musicians.component.ts musicians.component.html <h1>Find Your Favorite Musicians=/h1> <app-search-box [query]="query$ | async"

    (search)="onSearch($event)" >=/app-search-box> <app-musician-list [musicians]="musicians$ | async" [isLoading]="isLoading$ | async" >=/app-musician-list> import { Store } from '@ngrx/store'; import * as musiciansPageActions from './musicians-page.actions'; import { selectFilteredMusicians, selectIsLoading, selectQuery, } from './musicians.selectors'; @Component({ selector: 'app-musicians', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [AsyncPipe, SearchBoxComponent, MusicianListComponent], templateUrl: './musicians.component.html', }) export class MusiciansComponent implements OnInit { private readonly store = inject(Store); readonly musicians$ = this.store.select(selectFilteredMusicians); readonly isLoading$ = this.store.select(selectIsLoading); readonly query$ = this.store.select(selectQuery); ngOnInit(): void { this.store.dispatch(musiciansPageActions.opened()); } onSearch(query: string): void { this.store.dispatch( musiciansPageActions.queryChanged({ query }) ); } }
  25. musicians.effects.ts import { Actions } from '@ngrx/effects'; @Injectable() export class

    MusiciansEffects { private readonly actions$ = inject(Actions); }
  26. musicians.effects.ts import { Actions, createEffect } from '@ngrx/effects'; @Injectable() export

    class MusiciansEffects { private readonly actions$ = inject(Actions); readonly loadAllMusicians$ = createEffect(() => { }); }
  27. musicians.effects.ts import { Actions, createEffect } from '@ngrx/effects'; @Injectable() export

    class MusiciansEffects { private readonly actions$ = inject(Actions); readonly loadAllMusicians$ = createEffect(() => { return this.actions$.pipe( ); }); }
  28. musicians.effects.ts import { Actions, createEffect, ofType } from '@ngrx/effects'; import

    * as musiciansPageActions from './musicians-page.actions'; @Injectable() export class MusiciansEffects { private readonly actions$ = inject(Actions); readonly loadAllMusicians$ = createEffect(() => { return this.actions$.pipe( ofType(musiciansPageActions.opened) ); }); }
  29. musicians.effects.ts import { Actions, createEffect, ofType } from '@ngrx/effects'; import

    * as musiciansPageActions from './musicians-page.actions'; @Injectable() export class MusiciansEffects { private readonly actions$ = inject(Actions); private readonly musiciansService = inject(MusiciansService); readonly loadAllMusicians$ = createEffect(() => { return this.actions$.pipe( ofType(musiciansPageActions.opened), exhaustMap(() => { return this.musiciansService.getAll().pipe( ); }) ); }); }
  30. musicians.effects.ts import { Actions, createEffect, ofType } from '@ngrx/effects'; import

    * as musiciansPageActions from './musicians-page.actions'; import * as musiciansApiActions from './musicians-api.actions'; @Injectable() export class MusiciansEffects { private readonly actions$ = inject(Actions); private readonly musiciansService = inject(MusiciansService); readonly loadAllMusicians$ = createEffect(() => { return this.actions$.pipe( ofType(musiciansPageActions.opened), exhaustMap(() => { return this.musiciansService.getAll().pipe( map((musicians) => musiciansApiActions.musiciansLoadedSuccess({ musicians }) ), ); }) ); }); }
  31. musicians.effects.ts import { Actions, createEffect, ofType } from '@ngrx/effects'; import

    * as musiciansPageActions from './musicians-page.actions'; import * as musiciansApiActions from './musicians-api.actions'; @Injectable() export class MusiciansEffects { private readonly actions$ = inject(Actions); private readonly musiciansService = inject(MusiciansService); readonly loadAllMusicians$ = createEffect(() => { return this.actions$.pipe( ofType(musiciansPageActions.opened), exhaustMap(() => { return this.musiciansService.getAll().pipe( map((musicians) => musiciansApiActions.musiciansLoadedSuccess({ musicians }) ), catchError(({ message }: { message: string }) => of(musiciansApiActions.musiciansLoadedFailure({ message })) ) ); }) ); }); }
  32. main.ts import { bootstrapApplication } from '@angular/platform-browser'; import { provideState,

    provideStore } from '@ngrx/store'; import * as fromMusicians from './app/musicians/musicians.reducer'; bootstrapApplication(AppComponent, { providers: [ provideStore(), provideState('musicians', fromMusicians.reducer), ], }); Registering Effects
  33. main.ts import { bootstrapApplication } from '@angular/platform-browser'; import { provideState,

    provideStore } from '@ngrx/store'; import * as fromMusicians from './app/musicians/musicians.reducer'; import { provideEffects } from '@ngrx/effects'; import { MusiciansEffects } from './app/musicians/musicians.effects'; bootstrapApplication(AppComponent, { providers: [ provideStore(), provideState('musicians', fromMusicians.reducer), provideEffects(MusiciansEffects), ], }); Registering Effects
  34. musicians/actions/musicians-api.actions.ts import { createAction, props } from '@ngrx/store'; export const

    musiciansLoadedSuccess = createAction( '[Musicians API] Musicians Loaded Success', props<{ musicians: Musician[] }>() ); export const musiciansLoadedFailure = createAction( '[Musicians API] Musicians Loaded Failure', props<{ message: string }>() ); createAction
  35. musicians/actions/musicians-api.actions.ts import { createAction, props } from '@ngrx/store'; export const

    musiciansLoadedSuccess = createAction( '[Musicians API] Musicians Loaded Success', props<{ musicians: Musician[] }>() ); export const musiciansLoadedFailure = createAction( '[Musicians API] Musicians Loaded Failure', props<{ message: string }>() ); createAction 1. Manually apply “[Source] Event” pattern
  36. musicians/actions/musicians-api.actions.ts import { createAction, props } from '@ngrx/store'; export const

    musiciansLoadedSuccess = createAction( '[Musicians API] Musicians Loaded Success', props<{ musicians: Musician[] }>() ); export const musiciansLoadedFailure = createAction( '[Musicians API] Musicians Loaded Failure', props<{ message: string }>() ); createAction 1. Manually apply “[Source] Event” pattern 2. Repeat “Source” for each action
  37. musicians/actions/musicians-api.actions.ts import { createAction, props } from '@ngrx/store'; export const

    musiciansLoadedSuccess = createAction( '[Musicians API] Musicians Loaded Success', props<{ musicians: Musician[] }>() ); export const musiciansLoadedFailure = createAction( '[Musicians API] Musicians Loaded Failure', props<{ message: string }>() ); createAction 1. Manually apply “[Source] Event” pattern 2. Repeat “Source” for each action 3. Action name is camel-cased event name
  38. musicians/actions/musicians-api.actions.ts import { createAction, props } from '@ngrx/store'; export const

    musiciansLoadedSuccess = createAction( '[Musicians API] Musicians Loaded Success', props<{ musicians: Musician[] }>() ); export const musiciansLoadedFailure = createAction( '[Musicians API] Musicians Loaded Failure', props<{ message: string }>() ); createAction musicians/actions/index.ts export * as musiciansApiActions from './musicians-api.actions'; 1. Manually apply “[Source] Event” pattern 2. Repeat “Source” for each action 3. Action name is camel-cased event name 4. Use barrel file to create named exports for actions with the same source
  39. musicians/actions/musicians-api.actions.ts import { createAction, props } from '@ngrx/store'; export const

    musiciansLoadedSuccess = createAction( '[Musicians API] Musicians Loaded Success', props<{ musicians: Musician[] }>() ); export const musiciansLoadedFailure = createAction( '[Musicians API] Musicians Loaded Failure', props<{ message: string }>() ); createAction musicians/actions/index.ts export * as musiciansApiActions from './musicians-api.actions'; createActionGroup musicians/actions/musicians-api.actions.ts
  40. musicians/actions/musicians-api.actions.ts import { createAction, props } from '@ngrx/store'; export const

    musiciansLoadedSuccess = createAction( '[Musicians API] Musicians Loaded Success', props<{ musicians: Musician[] }>() ); export const musiciansLoadedFailure = createAction( '[Musicians API] Musicians Loaded Failure', props<{ message: string }>() ); createAction musicians/actions/index.ts export * as musiciansApiActions from './musicians-api.actions'; createActionGroup musicians/actions/musicians-api.actions.ts import { createActionGroup } from '@ngrx/store'; export const musiciansApiActions = createActionGroup({ });
  41. musicians/actions/musicians-api.actions.ts import { createAction, props } from '@ngrx/store'; export const

    musiciansLoadedSuccess = createAction( '[Musicians API] Musicians Loaded Success', props<{ musicians: Musician[] }>() ); export const musiciansLoadedFailure = createAction( '[Musicians API] Musicians Loaded Failure', props<{ message: string }>() ); createAction musicians/actions/index.ts export * as musiciansApiActions from './musicians-api.actions'; createActionGroup musicians/actions/musicians-api.actions.ts import { createActionGroup } from '@ngrx/store'; export const musiciansApiActions = createActionGroup({ source: 'Musicians API', });
  42. musicians/actions/musicians-api.actions.ts import { createAction, props } from '@ngrx/store'; export const

    musiciansLoadedSuccess = createAction( '[Musicians API] Musicians Loaded Success', props<{ musicians: Musician[] }>() ); export const musiciansLoadedFailure = createAction( '[Musicians API] Musicians Loaded Failure', props<{ message: string }>() ); createAction musicians/actions/index.ts export * as musiciansApiActions from './musicians-api.actions'; createActionGroup musicians/actions/musicians-api.actions.ts import { createActionGroup, props } from '@ngrx/store'; export const musiciansApiActions = createActionGroup({ source: 'Musicians API', events: { 'Musicians Loaded Success': props<{ musicians: Musician[] }>(), 'Musicians Loaded Failure': props<{ message: string }>(), }, });
  43. musicians/actions/musicians-api.actions.ts import { createAction, props } from '@ngrx/store'; export const

    musiciansLoadedSuccess = createAction( '[Musicians API] Musicians Loaded Success', props<{ musicians: Musician[] }>() ); export const musiciansLoadedFailure = createAction( '[Musicians API] Musicians Loaded Failure', props<{ message: string }>() ); createAction musicians/actions/index.ts export * as musiciansApiActions from './musicians-api.actions'; createActionGroup musicians/actions/musicians-api.actions.ts import { createActionGroup, props } from '@ngrx/store'; export const musiciansApiActions = createActionGroup({ source: 'Musicians API', events: { 'Musicians Loaded Success': props<{ musicians: Musician[] }>(), 'Musicians Loaded Failure': props<{ message: string }>(), }, }); */ type: [Musicians API] Musicians Loaded Success musiciansApiActions.musiciansLoadedSuccess({ musicians }); */ type: [Musicians API] Musicians Loaded Failure musiciansApiActions.musiciansLoadedFailure({ message });
  44. musicians-api.actions.ts export const musiciansLoadedSuccess = createAction( '[Musicians API] Musicians Loaded

    Success', props<{ musicians: Musician[] }>() ); export const musiciansLoadedFailure = createAction( '[Musicians API] Musicians Loaded Failure', props<{ message: string }>() ); export const musicianCreatedSuccess = createAction( '[Musicians API] Musician Created Success', props<{ musician: Musician }>() ); export const musicianCreatedFailure = createAction( '[Musicians API] Musician Created Failure', props<{ message: string }>() ); export const musicianUpdatedSuccess = createAction( '[Musicians API] Musician Updated Success', props<{ musician: Musician }>() ); export const musicianUpdatedFailure = createAction( '[Musicians API] Musician Updated Failure', props<{ message: string }>() ); export const musiciansApiActions = createActionGroup({ source: 'Musicians API', events: { 'Musicians Loaded Success': props<{ musicians: Musician[] }>(), 'Musicians Loaded Failure': props<{ message: string }>(), 'Musician Created Success': props<{ musician: Musician }>(), 'Musician Created Failure': props<{ message: string }>(), 'Musician Updated Success': props<{ musician: Musician }>(), 'Musician Updated Failure': props<{ message: string }>(), }, }); ~50%
  45. musicians.state.ts interface State { musicians: Musician[]; isLoading: boolean; query: string;

    } const initialState = { musicians: [], isLoading: false, query: '' }; const reducer = createReducer(initialState, ** **. */); musicians.selectors.ts import { createFeatureSelector, createSelector, } from '@ngrx/store'; import * as fromMusicians from './musicians.reducer'; export const selectMusiciansState = createFeatureSelector<fromMusicians.State>('musicians'); export const selectMusicians = createSelector( selectMusiciansState, (state) => state.musicians ); export const selectIsLoading = createSelector( selectMusiciansState, (state) => state.isLoading ); export const selectQuery = createSelector( selectMusiciansState, (state) => state.query ); export const selectFilteredMusicians = createSelector( selectMusicians, selectQuery, (musicians, query) => musicians.filter(({ name }) => name.includes(query)) );
  46. musicians.state.ts import { createFeature } from '@ngrx/store'; interface State {

    musicians: Musician[]; isLoading: boolean; query: string; } const initialState = { musicians: [], isLoading: false, query: '' }; const reducer = createReducer(initialState, ** **. */); export const musiciansFeature = createFeature({ name: 'musicians', reducer, }); musicians.selectors.ts import { createFeatureSelector, createSelector, } from '@ngrx/store'; import * as fromMusicians from './musicians.reducer'; export const selectMusiciansState = createFeatureSelector<fromMusicians.State>('musicians'); export const selectMusicians = createSelector( selectMusiciansState, (state) => state.musicians ); export const selectIsLoading = createSelector( selectMusiciansState, (state) => state.isLoading ); export const selectQuery = createSelector( selectMusiciansState, (state) => state.query ); export const selectFilteredMusicians = createSelector( selectMusicians, selectQuery, (musicians, query) => musicians.filter(({ name }) => name.includes(query)) );
  47. musicians.state.ts import { createFeature } from '@ngrx/store'; interface State {

    musicians: Musician[]; isLoading: boolean; query: string; } const initialState = { musicians: [], isLoading: false, query: '' }; const reducer = createReducer(initialState, ** **. */); export const musiciansFeature = createFeature({ name: 'musicians', reducer, }); const { name, reducer, selectMusiciansState, */ feature selector */ selector for each feature state property selectMusicians, selectIsLoading, selectQuery, } = musiciansFeature; musicians.selectors.ts import { createFeatureSelector, createSelector, } from '@ngrx/store'; import * as fromMusicians from './musicians.reducer'; export const selectMusiciansState = createFeatureSelector<fromMusicians.State>('musicians'); export const selectMusicians = createSelector( selectMusiciansState, (state) => state.musicians ); export const selectIsLoading = createSelector( selectMusiciansState, (state) => state.isLoading ); export const selectQuery = createSelector( selectMusiciansState, (state) => state.query ); export const selectFilteredMusicians = createSelector( selectMusicians, selectQuery, (musicians, query) => musicians.filter(({ name }) => name.includes(query)) );
  48. musicians.state.ts import { createFeature } from '@ngrx/store'; interface State {

    musicians: Musician[]; isLoading: boolean; query: string; } const initialState = { musicians: [], isLoading: false, query: '' }; const reducer = createReducer(initialState, ** **. */); export const musiciansFeature = createFeature({ name: 'musicians', reducer, }); const { name, reducer, selectMusiciansState, */ feature selector */ selector for each feature state property selectMusicians, selectIsLoading, selectQuery, } = musiciansFeature; musicians.selectors.ts import { createFeatureSelector, createSelector, } from '@ngrx/store'; import * as fromMusicians from './musicians.reducer'; export const selectMusiciansState = createFeatureSelector<fromMusicians.State>('musicians'); export const selectMusicians = createSelector( selectMusiciansState, (state) => state.musicians ); export const selectIsLoading = createSelector( selectMusiciansState, (state) => state.isLoading ); export const selectQuery = createSelector( selectMusiciansState, (state) => state.query ); export const selectFilteredMusicians = createSelector( selectMusicians, selectQuery, (musicians, query) => musicians.filter(({ name }) => name.includes(query)) );
  49. musicians.state.ts import { createFeature } from '@ngrx/store'; interface State {

    musicians: Musician[]; isLoading: boolean; query: string; } const initialState = { musicians: [], isLoading: false, query: '' }; const reducer = createReducer(initialState, ** **. */); export const musiciansFeature = createFeature({ name: 'musicians', reducer, extraSelectors: ({ selectMusicians, selectQuery }) => ({ selectFilteredMusicians: createSelector( selectMusicians, selectQuery, (musicians, query) => musicians.filter(({ name }) => name.includes(query)) ), }), }); const { name, reducer, selectMusiciansState, */ feature selector */ selector for each feature state property selectMusicians, selectIsLoading, selectQuery, selectFilteredMusicians, */ extra selectors } = musiciansFeature; musicians.selectors.ts import { createFeatureSelector, createSelector, } from '@ngrx/store'; import * as fromMusicians from './musicians.reducer'; export const selectMusiciansState = createFeatureSelector<fromMusicians.State>('musicians'); export const selectMusicians = createSelector( selectMusiciansState, (state) => state.musicians ); export const selectIsLoading = createSelector( selectMusiciansState, (state) => state.isLoading ); export const selectQuery = createSelector( selectMusiciansState, (state) => state.query ); export const selectFilteredMusicians = createSelector( selectMusicians, selectQuery, (musicians, query) => musicians.filter(({ name }) => name.includes(query)) );
  50. musicians.state.ts import { createFeature } from '@ngrx/store'; interface State {

    musicians: Musician[]; isLoading: boolean; query: string; } const initialState = { musicians: [], isLoading: false, query: '' }; const reducer = createReducer(initialState, ** **. */); export const musiciansFeature = createFeature({ name: 'musicians', reducer, extraSelectors: ({ selectMusicians, selectQuery }) => ({ selectFilteredMusicians: createSelector( selectMusicians, selectQuery, (musicians, query) => musicians.filter(({ name }) => name.includes(query)) ), }), }); const { name, reducer, selectMusiciansState, */ feature selector */ selector for each feature state property selectMusicians, selectIsLoading, selectQuery, selectFilteredMusicians, */ extra selectors } = musiciansFeature; musicians.selectors.ts import { createFeatureSelector, createSelector, } from '@ngrx/store'; import * as fromMusicians from './musicians.reducer'; export const selectMusiciansState = createFeatureSelector<fromMusicians.State>('musicians'); export const selectMusicians = createSelector( selectMusiciansState, (state) => state.musicians ); export const selectIsLoading = createSelector( selectMusiciansState, (state) => state.isLoading ); export const selectQuery = createSelector( selectMusiciansState, (state) => state.query ); export const selectFilteredMusicians = createSelector( selectMusicians, selectQuery, (musicians, query) => musicians.filter(({ name }) => name.includes(query)) );
  51. musicians.state.ts import { createFeature } from '@ngrx/store'; interface State {

    musicians: Musician[]; isLoading: boolean; query: string; } const initialState = { musicians: [], isLoading: false, query: '' }; const reducer = createReducer(initialState, ** **. */); export const musiciansFeature = createFeature({ name: 'musicians', reducer, extraSelectors: ({ selectMusicians, selectQuery }) => ({ selectFilteredMusicians: createSelector( selectMusicians, selectQuery, (musicians, query) => musicians.filter(({ name }) => name.includes(query)) ), }), }); main.ts import { musiciansFeature } from './app/musicians.state'; bootstrapApplication(AppComponent, { providers: [ provideStore(), provideState(musiciansFeature), ], });
  52. musicians.effects.ts import { Actions, createEffect } from '@ngrx/effects'; export const

    loadAllMusicians = createEffect( ( actions$ = inject(Actions), musiciansService = inject(MusiciansService) ) => { }, );
  53. musicians.effects.ts import { Actions, createEffect, ofType } from '@ngrx/effects'; import

    { musiciansPageActions } from './musicians-page.actions'; import { musiciansApiActions } from './musicians-api.actions'; export const loadAllMusicians = createEffect( ( actions$ = inject(Actions), musiciansService = inject(MusiciansService) ) => { return actions$.pipe( ofType(musiciansPageActions.opened), exhaustMap(() => { return musiciansService.getAll().pipe( map((musicians) => musiciansApiActions.musiciansLoadedSuccess({ musicians }) ), catchError(({ message }: { message: string }) => of(musiciansApiActions.musiciansLoadedFailure({ message })) ) ); }) ); }, );
  54. musicians.effects.ts import { Actions, createEffect, ofType } from '@ngrx/effects'; import

    { musiciansPageActions } from './musicians-page.actions'; import { musiciansApiActions } from './musicians-api.actions'; export const loadAllMusicians = createEffect( ( actions$ = inject(Actions), musiciansService = inject(MusiciansService) ) => { return actions$.pipe( ofType(musiciansPageActions.opened), exhaustMap(() => { return musiciansService.getAll().pipe( map((musicians) => musiciansApiActions.musiciansLoadedSuccess({ musicians }) ), catchError(({ message }: { message: string }) => of(musiciansApiActions.musiciansLoadedFailure({ message })) ) ); }) ); }, { functional: true } );
  55. musicians.effects.ts import { Actions, createEffect, ofType } from '@ngrx/effects'; import

    { musiciansPageActions } from './musicians-page.actions'; import { musiciansApiActions } from './musicians-api.actions'; export const loadAllMusicians = createEffect( ( actions$ = inject(Actions), musiciansService = inject(MusiciansService) ) => { return actions$.pipe( ofType(musiciansPageActions.opened), exhaustMap(() => { return musiciansService.getAll().pipe( map((musicians) => musiciansApiActions.musiciansLoadedSuccess({ musicians }) ), catchError(({ message }: { message: string }) => of(musiciansApiActions.musiciansLoadedFailure({ message })) ) ); }) ); }, { functional: true } ); main.ts import * as musiciansEffects from './app/musicians.effects'; bootstrapApplication(AppComponent, { providers: [ provideStore(), provideState(musiciansFeature), provideEffects(musiciansEffects), ], });
  56. musicians.effects.ts import { Actions, createEffect, ofType } from '@ngrx/effects'; import

    { musiciansPageActions } from './musicians-page.actions'; import { musiciansApiActions } from './musicians-api.actions'; export const loadAllMusicians = createEffect( ( actions$ = inject(Actions), musiciansService = inject(MusiciansService) ) => { return actions$.pipe( ofType(musiciansPageActions.opened), exhaustMap(() => { return musiciansService.getAll().pipe( map((musicians) => musiciansApiActions.musiciansLoadedSuccess({ musicians }) ), catchError(({ message }: { message: string }) => of(musiciansApiActions.musiciansLoadedFailure({ message })) ) ); }) ); }, { functional: true } ); main.ts import * as musiciansEffects from './app/musicians.effects'; bootstrapApplication(AppComponent, { providers: [ provideStore(), provideState(musiciansFeature), provideEffects(musiciansEffects), ], }); musicians.effects.spec.ts import { loadAllMusicians } from './app/musicians.effects'; const result$ = loadAllMusicians( actionsMock, musiciansServiceMock ); expect(result$).toBeObservable(** **. */); expect(musiciansServiceMock, 'getAll').toHaveBeenCalled();