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

NgRx Tips for Future-Proof Angular Apps

Marko Stanimirović
April 14, 2022
120

NgRx Tips for Future-Proof Angular Apps

Marko Stanimirović

April 14, 2022
Tweet

Transcript

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

    NgRx Team Member ★ Angular Belgrade Organizer ★ Hobby Musician ★ M.Sc. in Software Engineering
  2. ★ Put global state in a single place ★ Use

    selectors for derived state ★ Use view model selectors ★ Create reusable reducers ★ Treat actions as unique events ★ Group actions by source Store Tips
  3. ★ Put global state in a single place ★ Use

    selectors for derived state ★ Use view model selectors ★ Create reusable reducers ★ Treat actions as unique events ★ Group actions by source Store Tips
  4. SongsService songs$: Observable<Song[]>; activeSong$: Observable<Song | null>; SongsComponent songsWithComposers$ =

    combineLatest([ ]) NgRx Store composers: { entities: Dictionary<Composer>; };
  5. SongsService songs$: Observable<Song[]>; activeSong$: Observable<Song | null>; SongsComponent songsWithComposers$ =

    combineLatest([ this.songsService.songs$, ]) NgRx Store composers: { entities: Dictionary<Composer>; };
  6. SongsService songs$: Observable<Song[]>; activeSong$: Observable<Song | null>; SongsComponent songsWithComposers$ =

    combineLatest([ this.songsService.songs$, this.store.select(selectComposers), ]) NgRx Store composers: { entities: Dictionary<Composer>; };
  7. SongsService songs$: Observable<Song[]>; activeSong$: Observable<Song | null>; SongsComponent songsWithComposers$ =

    combineLatest([ this.songsService.songs$, this.store.select(selectComposers), ]).pipe( map(([songs, composers]) .> songs.map((song) .> ({ ...song, composer: composers[song.composerId], })) ) ); NgRx Store composers: { entities: Dictionary<Composer>; };
  8. SongsService songs$: Observable<Song[]>; activeSong$: Observable<Song | null>; SongsFacade songsWithComposers$ =

    NgRx Store composers: { entities: Dictionary<Composer>; }; SongsComponent songsWithComposers$ =
  9. SongsService songs$: Observable<Song[]>; activeSong$: Observable<Song | null>; SongsFacade songsWithComposers$ =

    combineLatest([ this.songsService.songs$, this.store.select(selectComposers), ]).pipe( map(([songs, composers]) .> songs.map((song) .> ({ ...song, composer: composers[song.composerId], })) ) ); NgRx Store composers: { entities: Dictionary<Composer>; }; SongsComponent songsWithComposers$ =
  10. SongsService songs$: Observable<Song[]>; activeSong$: Observable<Song | null>; SongsFacade songsWithComposers$ =

    combineLatest([ this.songsService.songs$, this.store.select(selectComposers), ]).pipe( map(([songs, composers]) .> songs.map((song) .> ({ ...song, composer: composers[song.composerId], })) ) ); NgRx Store composers: { entities: Dictionary<Composer>; }; SongsComponent songsWithComposers$ = this.facade.songsWithComposers$;
  11. SongsService songs$: Observable<Song[]>; activeSong$: Observable<Song | null>; SongsFacade songsWithComposers$ =

    combineLatest([ this.songsService.songs$, this.store.select(selectComposers), ]).pipe( map(([songs, composers]) .> songs.map((song) .> ({ ...song, composer: composers[song.composerId], })) ) ); NgRx Store composers: { entities: Dictionary<Composer>; }; SongsComponent songsWithComposers$ = this.facade.songsWithComposers$;
  12. NgRx Store songs: { entities: Dictionary<Song>; activeId: string | null;

    }; composers: { entities: Dictionary<Composer>; };
  13. NgRx Store songs: { entities: Dictionary<Song>; activeId: string | null;

    }; composers: { entities: Dictionary<Composer>; }; songs.selectors.ts const selectSongsWithComposers =
  14. NgRx Store songs: { entities: Dictionary<Song>; activeId: string | null;

    }; composers: { entities: Dictionary<Composer>; }; songs.selectors.ts const selectSongsWithComposers = createSelector( selectAllSongs, );
  15. NgRx Store songs: { entities: Dictionary<Song>; activeId: string | null;

    }; composers: { entities: Dictionary<Composer>; }; songs.selectors.ts const selectSongsWithComposers = createSelector( selectAllSongs, selectComposers, );
  16. NgRx Store songs: { entities: Dictionary<Song>; activeId: string | null;

    }; composers: { entities: Dictionary<Composer>; }; songs.selectors.ts const selectSongsWithComposers = createSelector( selectAllSongs, selectComposers, (songs, composers) .> songs.map((song) .> ({ ...song, composer: composers[song.composerId], })) );
  17. NgRx Store songs: { entities: Dictionary<Song>; activeId: string | null;

    }; composers: { entities: Dictionary<Composer>; }; songs.selectors.ts const selectSongsWithComposers = createSelector( selectAllSongs, selectComposers, (songs, composers) .> songs.map((song) .> ({ ...song, composer: composers[song.composerId], })) ); SongsComponent songsWithComposers$ = this.store.select(selectSongsWithComposers);
  18. ★ Put global state in a single place ★ Use

    selectors for derived state ★ Use view model selectors ★ Create reusable reducers ★ Treat actions as unique events ★ Group actions by source Store Tips
  19. export const musiciansReducer = createReducer( on(musiciansPageActions.search, (state, { query })

    .> { const filteredMusicians = state.musicians.filter(({ name }) .> name.includes(query) ); return { ...state, query, filteredMusicians, }; }) );
  20. export const musiciansReducer = createReducer( on(musiciansPageActions.search, (state, { query })

    .> { const filteredMusicians = state.musicians.filter(({ name }) .> name.includes(query) ); return { ...state, query, filteredMusicians, }; }) );
  21. export const selectFilteredMusicians = createSelector( selectAllMusicians, selectMusicianQuery, (musicians, query) .>

    musicians.filter(({ name }) .> name.includes(query)) ); export const musiciansReducer = createReducer( on(musiciansPageActions.search, (state, { query }) .> ({ ...state, query, })) );
  22. ★ Put global state in a single place ★ Use

    selectors for derived state ★ Use view model selectors ★ Create reusable reducers ★ Treat actions as unique events ★ Group actions by source Store Tips
  23. musicians.component.ts @Component({ selector: 'musicians', template: ` <musician-search [query]="query$ | async">./musician-search>

    <musician-list [musicians]="musicians$ | async" [activeMusician]="activeMusician$ | async" >./musician-list> <musician-details [musician]="activeMusician$ | async" >./musician-details> `, }) export class MusiciansComponent { readonly musicians$ = this.store.select(selectFilteredMusicians); readonly query$ = this.store.select(selectMusiciansQuery); readonly activeMusician$ = this.store.select(selectActiveMusician); constructor(private readonly store: Store) {} }
  24. musicians.component.ts @Component({ selector: 'musicians', template: ` <musician-search [query]="query$ | async">./musician-search>

    <musician-list [musicians]="musicians$ | async" [activeMusician]="activeMusician$ | async" >./musician-list> <musician-details [musician]="activeMusician$ | async" >./musician-details> `, }) export class MusiciansComponent { readonly musicians$ = this.store.select(selectFilteredMusicians); readonly query$ = this.store.select(selectMusiciansQuery); readonly activeMusician$ = this.store.select(selectActiveMusician); constructor(private readonly store: Store) {} }
  25. musicians.component.ts @Component({ selector: 'musicians', template: ` <musician-search [query]="query$ | async">./musician-search>

    <musician-list [musicians]="musicians$ | async" [activeMusician]="activeMusician$ | async" >./musician-list> <musician-details [musician]="activeMusician$ | async" >./musician-details> `, }) export class MusiciansComponent { readonly musicians$ = this.store.select(selectFilteredMusicians); readonly query$ = this.store.select(selectMusiciansQuery); readonly activeMusician$ = this.store.select(selectActiveMusician); constructor(private readonly store: Store) {} }
  26. musicians.component.ts @Component({ selector: 'musicians', template: ` <musician-search [query]="query$ | async">./musician-search>

    <musician-list [musicians]="musicians$ | async" [activeMusician]="activeMusician$ | async" >./musician-list> <musician-details [musician]="activeMusician$ | async" >./musician-details> `, }) export class MusiciansComponent { readonly musicians$ = this.store.select(selectFilteredMusicians); readonly query$ = this.store.select(selectMusiciansQuery); readonly activeMusician$ = this.store.select(selectActiveMusician); constructor(private readonly store: Store) {} }
  27. musicians.component.ts @Component({ selector: 'musicians', template: ` <musician-search [query]="query$ | async">./musician-search>

    <musician-list [musicians]="musicians$ | async" [activeMusician]="activeMusician$ | async" >./musician-list> <musician-details [musician]="activeMusician$ | async" >./musician-details> `, }) export class MusiciansComponent { readonly musicians$ = this.store.select(selectFilteredMusicians); readonly query$ = this.store.select(selectMusiciansQuery); readonly activeMusician$ = this.store.select(selectActiveMusician); constructor(private readonly store: Store) {} } musicians.selectors.ts export const selectMusiciansPageViewModel = createSelector( );
  28. musicians.component.ts @Component({ selector: 'musicians', template: ` <musician-search [query]="query$ | async">./musician-search>

    <musician-list [musicians]="musicians$ | async" [activeMusician]="activeMusician$ | async" >./musician-list> <musician-details [musician]="activeMusician$ | async" >./musician-details> `, }) export class MusiciansComponent { readonly musicians$ = this.store.select(selectFilteredMusicians); readonly query$ = this.store.select(selectMusiciansQuery); readonly activeMusician$ = this.store.select(selectActiveMusician); constructor(private readonly store: Store) {} } musicians.selectors.ts export const selectMusiciansPageViewModel = createSelector( selectFilteredMusicians, );
  29. musicians.component.ts @Component({ selector: 'musicians', template: ` <musician-search [query]="query$ | async">./musician-search>

    <musician-list [musicians]="musicians$ | async" [activeMusician]="activeMusician$ | async" >./musician-list> <musician-details [musician]="activeMusician$ | async" >./musician-details> `, }) export class MusiciansComponent { readonly musicians$ = this.store.select(selectFilteredMusicians); readonly query$ = this.store.select(selectMusiciansQuery); readonly activeMusician$ = this.store.select(selectActiveMusician); constructor(private readonly store: Store) {} } musicians.selectors.ts export const selectMusiciansPageViewModel = createSelector( selectFilteredMusicians, selectMusiciansQuery, );
  30. musicians.component.ts @Component({ selector: 'musicians', template: ` <musician-search [query]="query$ | async">./musician-search>

    <musician-list [musicians]="musicians$ | async" [activeMusician]="activeMusician$ | async" >./musician-list> <musician-details [musician]="activeMusician$ | async" >./musician-details> `, }) export class MusiciansComponent { readonly musicians$ = this.store.select(selectFilteredMusicians); readonly query$ = this.store.select(selectMusiciansQuery); readonly activeMusician$ = this.store.select(selectActiveMusician); constructor(private readonly store: Store) {} } musicians.selectors.ts export const selectMusiciansPageViewModel = createSelector( selectFilteredMusicians, selectMusiciansQuery, selectActiveMusician, );
  31. musicians.component.ts @Component({ selector: 'musicians', template: ` <musician-search [query]="query$ | async">./musician-search>

    <musician-list [musicians]="musicians$ | async" [activeMusician]="activeMusician$ | async" >./musician-list> <musician-details [musician]="activeMusician$ | async" >./musician-details> `, }) export class MusiciansComponent { readonly musicians$ = this.store.select(selectFilteredMusicians); readonly query$ = this.store.select(selectMusiciansQuery); readonly activeMusician$ = this.store.select(selectActiveMusician); constructor(private readonly store: Store) {} } musicians.selectors.ts export const selectMusiciansPageViewModel = createSelector( selectFilteredMusicians, selectMusiciansQuery, selectActiveMusician, (musicians, query, activeMusician) .> ({ musicians, query, activeMusician, }) );
  32. musicians.component.ts @Component({ selector: 'musicians', template: ` <ng-container *ngIf="vm$ | async

    as vm"> <musician-search [query]="vm.query">./musician-search> <musician-list [musicians]="vm.musicians" [activeMusician]="vm.activeMusician" >./musician-list> <musician-details [musician]="vm.activeMusician" >./musician-details> ./ng-container> `, }) export class MusiciansComponent { readonly vm$ = this.store.select(selectMusiciansPageViewModel); constructor(private readonly store: Store) {} } musicians.selectors.ts export const selectMusiciansPageViewModel = createSelector( selectFilteredMusicians, selectMusiciansQuery, selectActiveMusician, (musicians, query, activeMusician) .> ({ musicians, query, activeMusician, }) );
  33. musicians.component.ts @Component({ selector: 'musicians', template: ` <ng-container *ngIf="vm$ | async

    as vm"> <musician-search [query]="vm.query">./musician-search> <musician-list [musicians]="vm.musicians" [activeMusician]="vm.activeMusician" >./musician-list> <musician-details [musician]="vm.activeMusician" >./musician-details> ./ng-container> `, }) export class MusiciansComponent { readonly vm$ = this.store.select(selectMusiciansPageViewModel); constructor(private readonly store: Store) {} } musicians.selectors.ts export const selectMusiciansPageViewModel = createSelector( selectFilteredMusicians, selectMusiciansQuery, selectActiveMusician, (musicians, query, activeMusician) .> ({ musicians, query, activeMusician, }) );
  34. musicians.component.ts @Component({ selector: 'musicians', template: ` <ng-container *ngIf="vm$ | async

    as vm"> <musician-search [query]="vm.query">./musician-search> <musician-list [musicians]="vm.musicians" [activeMusician]="vm.activeMusician" >./musician-list> <musician-details [musician]="vm.activeMusician" >./musician-details> ./ng-container> `, }) export class MusiciansComponent { readonly vm$ = this.store.select(selectMusiciansPageViewModel); constructor(private readonly store: Store) {} } musicians.selectors.ts export const selectMusiciansPageViewModel = createSelector( selectFilteredMusicians, selectMusiciansQuery, selectActiveMusician, (musicians, query, activeMusician) .> ({ musicians, query, activeMusician, }) );
  35. ★ Put global state in a single place ★ Use

    selectors for derived state ★ Use view model selectors ★ Create reusable reducers ★ Treat actions as unique events ★ Group actions by source Store Tips
  36. export const composersReducer = createReducer( initialState, on( composerExistsGuardActions.canActivate, composersPageActions.opened, songsPageActions.opened,

    (state, action) .> action.type ..= composerExistsGuardActions.canActivate.type .& state.entities[action.composerId] ? state : { ...state, isLoading: true } ) );
  37. export const composersReducer = createReducer( initialState, on(composersPageActions.opened, songsPageActions.opened, (state) .>

    ({ ...state, isLoading: true, })), on(composerExistsGuardActions.canActivate, (state, { composerId }) .> state.entities[composerId] ? state : { ...state, isLoading: true } ) );
  38. export const composersReducer = createReducer( initialState, on(composersPageActions.opened, songsPageActions.opened, (state) .>

    ({ ...state, isLoading: true, })), on(composerExistsGuardActions.canActivate, (state, { composerId }) .> state.entities[composerId] ? state : { ...state, isLoading: true } ) );
  39. ★ Put global state in a single place ★ Use

    selectors for derived state ★ Use view model selectors ★ Create reusable reducers ★ Treat actions as unique events ★ Group actions by source Store Tips
  40. @Component(** **. */) export class SongsComponent implements OnInit { readonly

    songs$ = this.store.select(selectSongs); constructor(private readonly store: Store) {} ngOnInit(): void { this.store.dispatch({ type: '[Songs] Load Songs' }); } }
  41. @Component(** **. */) export class SongsComponent implements OnInit { readonly

    songs$ = this.store.select(selectSongs); constructor(private readonly store: Store) {} ngOnInit(): void { this.store.dispatch({ type: '[Songs] Load Songs' }); } }
  42. @Component(** **. */) export class SongsComponent implements OnInit { readonly

    songs$ = this.store.select(selectSongsWithComposers); constructor(private readonly store: Store) {} ngOnInit(): void { this.store.dispatch({ type: '[Songs] Load Songs' }); this.store.dispatch({ type: '[Composers] Load Composers' }); } }
  43. @Component(** **. */) export class SongsComponent implements OnInit { readonly

    songs$ = this.store.select(selectSongsWithComposers); constructor(private readonly store: Store) {} ngOnInit(): void { this.store.dispatch({ type: '[Songs] Load Songs' }); this.store.dispatch({ type: '[Composers] Load Composers' }); } }
  44. @Component(** **. */) export class SongsComponent implements OnInit { readonly

    songs$ = this.store.select(selectSongsWithComposers); constructor(private readonly store: Store) {} ngOnInit(): void { this.store.dispatch({ type: '[Songs Page] Opened' }); } }
  45. @Component(** **. */) export class SongsComponent implements OnInit { readonly

    songs$ = this.store.select(selectSongsWithComposers); constructor(private readonly store: Store) {} ngOnInit(): void { this.store.dispatch({ type: '[Songs Page] Opened' }); } } source event
  46. [Login Page] Login Form Submitted [Auth API] User Logged in

    Successfully [Songs Page] Opened [Songs API] Songs Loaded Successfully [Composers API] Composers Loaded Successfully
  47. [Login Page] Login Form Submitted [Auth API] User Logged in

    Successfully [Songs Page] Opened [Songs API] Songs Loaded Successfully [Composers API] Composers Loaded Successfully [Auth] Login [Auth] Login Success [Songs] Load Songs [Composers] Load Composers [Songs] Load Songs Success [Composers] Load Composers Success
  48. ★ Put global state in a single place ★ Use

    selectors for derived state ★ Use view model selectors ★ Create reusable reducers ★ Treat actions as unique events ★ Group actions by source Store Tips
  49. songs-page.actions.ts export const opened = createAction('[Songs Page] Opened'); export const

    searchSongs = createAction( '[Songs Page] Search Songs Button Clicked', props<{ query: string }>() ); export const addComposer = createAction( '[Songs Page] Add Composer Form Submitted', props<{ composer: Composer }>() );
  50. songs-page.actions.ts export const opened = createAction('[Songs Page] Opened'); export const

    searchSongs = createAction( '[Songs Page] Search Songs Button Clicked', props<{ query: string }>() ); export const addComposer = createAction( '[Songs Page] Add Composer Form Submitted', props<{ composer: Composer }>() ); songs-api.actions.ts export const songsLoadedSuccess = createAction( '[Songs API] Songs Loaded Successfully', props<{ songs: Song[] }>() ); export const songsLoadedFailure = createAction( '[Songs API] Failed to Load Songs', props<{ errorMsg: string }>() );
  51. songs-page.actions.ts export const opened = createAction('[Songs Page] Opened'); export const

    searchSongs = createAction( '[Songs Page] Search Songs Button Clicked', props<{ query: string }>() ); export const addComposer = createAction( '[Songs Page] Add Composer Form Submitted', props<{ composer: Composer }>() ); songs-api.actions.ts export const songsLoadedSuccess = createAction( '[Songs API] Songs Loaded Successfully', props<{ songs: Song[] }>() ); export const songsLoadedFailure = createAction( '[Songs API] Failed to Load Songs', props<{ errorMsg: string }>() ); composer-exists-guard.actions.ts export const canActivate = createAction( '[Composer Exists Guard] Can Activate Entered', props<{ composerId: string }>() );
  52. ★ Put global state in a single place ★ Use

    selectors for derived state ★ Use view model selectors ★ Create reusable reducers ★ Treat actions as unique events ★ Group actions by source Store Tips
  53. ★ Name effects like functions ★ Keep effects simple ★

    Don’t create “boiler” effects ★ Apply good action hygiene Effects Tips
  54. ★ Name effects like functions ★ Keep effects simple ★

    Don’t create “boiler” effects ★ Apply good action hygiene Effects Tips
  55. readonly composerAddedSuccess$ = createEffect( () .> { return this.actions$.pipe( ofType(composersApiActions.composerAddedSuccess),

    tap(() .> this.alert.success('Composer saved successfully!')) ); }, { dispatch: false } );
  56. readonly composerAddedSuccess$ = createEffect( () .> { return this.actions$.pipe( ofType(composersApiActions.composerAddedSuccess),

    tap(() .> this.alert.success('Composer saved successfully!')) ); }, { dispatch: false } );
  57. readonly showSaveComposerSuccessAlert$ = createEffect( () .> { return this.actions$.pipe( ofType(composersApiActions.composerAddedSuccess),

    tap(() .> this.alert.success('Composer saved successfully!')) ); }, { dispatch: false } );
  58. readonly showSaveComposerSuccessAlert$ = createEffect( () .> { return this.actions$.pipe( ofType(

    composersApiActions.composerAddedSuccess, composersApiActions.composerUpdatedSuccess ), tap(() .> this.alert.success('Composer saved successfully!')) ); }, { dispatch: false } );
  59. readonly showSaveComposerSuccessAlert$ = createEffect( () .> { return this.actions$.pipe( ofType(

    composersApiActions.composerAddedSuccess, composersApiActions.composerUpdatedSuccess ), tap(() .> this.alert.success('Composer saved successfully!')) ); }, { dispatch: false } );
  60. ★ Name effects like functions ★ Keep effects simple ★

    Don’t create “boiler” effects ★ Apply good action hygiene Effects Tips
  61. readonly loadMusician$ = createEffect(() .> { return this.actions$.pipe( ofType(musicianDetailsPage.opened), concatLatestFrom(()

    .> this.store.select(selectMusicianIdFromRoute) ), concatMap(([, musicianId]) .> { return this.musiciansResource.getMusician(musicianId).pipe( mergeMap((musician) .> { return this.bandsResource.getBand(musician.bandId).pipe( map((band) .> ({ ...musician, bandName: band.name })) ); }), map((musician) .> musiciansApiActions.musicianLoadedSuccess({ musician }) ), catchError((error: { message: string }) .> of(musiciansApiActions.musicianLoadedFailure({ error })) ) ); }) ); });
  62. readonly loadMusician$ = createEffect(() .> { return this.actions$.pipe( ofType(musicianDetailsPage.opened), concatLatestFrom(()

    .> this.store.select(selectMusicianIdFromRoute) ), concatMap(([, musicianId]) .> { return this.musiciansResource.getMusician(musicianId).pipe( mergeMap((musician) .> { return this.bandsResource.getBand(musician.bandId).pipe( map((band) .> ({ ...musician, bandName: band.name })) ); }), map((musician) .> musiciansApiActions.musicianLoadedSuccess({ musician }) ), catchError((error: { message: string }) .> of(musiciansApiActions.musicianLoadedFailure({ error })) ) ); }) ); });
  63. readonly loadMusician$ = createEffect(() .> { return this.actions$.pipe( ofType(musicianDetailsPage.opened), concatLatestFrom(()

    .> this.store.select(selectMusicianIdFromRoute) ), concatMap(([, musicianId]) .> { return this.musiciansResource.getMusician(musicianId).pipe( mergeMap((musician) .> { return this.bandsResource.getBand(musician.bandId).pipe( map((band) .> ({ ...musician, bandName: band.name })) ); }), map((musician) .> musiciansApiActions.musicianLoadedSuccess({ musician }) ), catchError((error: { message: string }) .> of(musiciansApiActions.musicianLoadedFailure({ error })) ) ); }) ); });
  64. readonly loadMusician$ = createEffect(() .> { return this.actions$.pipe( ofType(musicianDetailsPage.opened), concatLatestFrom(()

    .> this.store.select(selectMusicianIdFromRoute) ), concatMap(([, musicianId]) .> { return this.musiciansResource.getMusician(musicianId).pipe( mergeMap((musician) .> { return this.bandsResource.getBand(musician.bandId).pipe( map((band) .> ({ ...musician, bandName: band.name })) ); }), map((musician) .> musiciansApiActions.musicianLoadedSuccess({ musician }) ), catchError((error: { message: string }) .> of(musiciansApiActions.musicianLoadedFailure({ error })) ) ); }) ); });
  65. readonly loadMusician$ = createEffect(() .> { return this.actions$.pipe( ofType(musicianDetailsPage.opened), concatLatestFrom(()

    .> this.store.select(selectMusicianIdFromRoute) ), concatMap(([, musicianId]) .> { return this.musiciansResource.getMusician(musicianId).pipe( mergeMap((musician) .> { return this.bandsResource.getBand(musician.bandId).pipe( map((band) .> ({ ...musician, bandName: band.name })) ); }), map((musician) .> musiciansApiActions.musicianLoadedSuccess({ musician }) ), catchError((error: { message: string }) .> of(musiciansApiActions.musicianLoadedFailure({ error })) ) ); }) ); });
  66. readonly loadMusician$ = createEffect(() .> { return this.actions$.pipe( ofType(musicianDetailsPage.opened), concatLatestFrom(()

    .> this.store.select(selectMusicianIdFromRoute) ), concatMap(([, musicianId]) .> { return this.musiciansResource.getMusician(musicianId).pipe( mergeMap((musician) .> { return this.bandsResource.getBand(musician.bandId).pipe( map((band) .> ({ ...musician, bandName: band.name })) ); }), map((musician) .> musiciansApiActions.musicianLoadedSuccess({ musician }) ), catchError((error: { message: string }) .> of(musiciansApiActions.musicianLoadedFailure({ error })) ) ); }) ); });
  67. readonly loadMusician$ = createEffect(() .> { return this.actions$.pipe( ofType(musicianDetailsPage.opened), concatLatestFrom(()

    .> this.store.select(selectMusicianIdFromRoute) ), concatMap(([, musicianId]) .> { return this.musiciansResource.getMusician(musicianId).pipe( mergeMap((musician) .> { return this.bandsResource.getBand(musician.bandId).pipe( map((band) .> ({ ...musician, bandName: band.name })) ); }), map((musician) .> musiciansApiActions.musicianLoadedSuccess({ musician }) ), catchError((error: { message: string }) .> of(musiciansApiActions.musicianLoadedFailure({ error })) ) ); }) ); });
  68. readonly loadMusician$ = createEffect(() .> { return this.actions$.pipe( ofType(musicianDetailsPage.opened), concatLatestFrom(()

    .> this.store.select(selectMusicianIdFromRoute) ), concatMap(([, musicianId]) .> { return this.musiciansResource.getMusician(musicianId).pipe( mergeMap((musician) .> { return this.bandsResource.getBand(musician.bandId).pipe( map((band) .> ({ ...musician, bandName: band.name })) ); }), map((musician) .> musiciansApiActions.musicianLoadedSuccess({ musician }) ), catchError((error: { message: string }) .> of(musiciansApiActions.musicianLoadedFailure({ error })) ) ); }) ); });
  69. @Injectable({ providedIn: 'root' }) export class MusiciansService { constructor( private

    readonly musiciansResource: MusiciansResource, private readonly bandsResource: BandsResource ) {} }
  70. @Injectable({ providedIn: 'root' }) export class MusiciansService { constructor( private

    readonly musiciansResource: MusiciansResource, private readonly bandsResource: BandsResource ) {} getMusician(musicianId: string): Observable<Musician> { return this.musiciansResource.getMusician(musicianId).pipe( mergeMap((musician) .> { return this.bandsResource.getBand(musician.bandId).pipe( map((band) .> ({ ...musician, bandName: band.name })) ); }) ); } }
  71. readonly loadMusician$ = createEffect(() .> { return this.actions$.pipe( ofType(musicianDetailsPage.opened), concatLatestFrom(()

    .> this.store.select(selectMusicianIdFromRoute) ), concatMap(([, musicianId]) .> { return this.musiciansResource.getMusician(musicianId).pipe( mergeMap((musician) .> { return this.bandsResource.getBand(musician.bandId).pipe( map((band) .> ({ ...musician, bandName: band.name })) ); }), map((musician) .> musiciansApiActions.musicianLoadedSuccess({ musician }) ), catchError((error: { message: string }) .> of(musiciansApiActions.musicianLoadedFailure({ error })) ) ); }) ); });
  72. readonly loadMusician$ = createEffect(() .> { return this.actions$.pipe( ofType(musicianDetailsPage.opened), concatLatestFrom(()

    .> this.store.select(selectMusicianIdFromRoute) ), concatMap(([, musicianId]) .> { return this.musiciansService.getMusician(musicianId).pipe( map((musician) .> musiciansApiActions.musicianLoadedSuccess({ musician }) ), catchError((error: { message: string }) .> of(musiciansApiActions.musicianLoadedFailure({ error })) ) ); }) ); });
  73. readonly loadMusician$ = createEffect(() .> { return this.actions$.pipe( ofType(musicianDetailsPage.opened), concatLatestFrom(()

    .> this.store.select(selectMusicianIdFromRoute) ), concatMap(([, musicianId]) .> { return this.musiciansService.getMusician(musicianId).pipe( map((musician) .> musiciansApiActions.musicianLoadedSuccess({ musician }) ), catchError((error: { message: string }) .> of(musiciansApiActions.musicianLoadedFailure({ error })) ) ); }) ); });
  74. ★ Name effects like functions ★ Keep effects simple ★

    Don’t create “boiler” effects ★ Apply good action hygiene Effects Tips
  75. readonly invokeLoadMusicians$ = createEffect(() .> { return this.actions$.pipe( ofType( musiciansPageActions.currentPageChanged,

    musiciansPageActions.pageSizeChanged ), map(() .> musiciansActions.loadMusicians()) ); });
  76. readonly invokeLoadMusicians$ = createEffect(() .> { return this.actions$.pipe( ofType( musiciansPageActions.currentPageChanged,

    musiciansPageActions.pageSizeChanged ), map(() .> musiciansActions.loadMusicians()) ); });
  77. readonly invokeLoadMusicians$ = createEffect(() .> { return this.actions$.pipe( ofType( musiciansPageActions.currentPageChanged,

    musiciansPageActions.pageSizeChanged ), map(() .> musiciansActions.loadMusicians()) ); });
  78. readonly invokeLoadMusicians$ = createEffect(() .> { return this.actions$.pipe( ofType( musiciansPageActions.currentPageChanged,

    musiciansPageActions.pageSizeChanged ), map(() .> musiciansActions.loadMusicians()) ); }); readonly loadMusicians$ = createEffect(() .> { return this.actions$.pipe( ofType(musiciansActions.loadMusicians), concatLatestFrom(() .> this.store.select(selectMusiciansPagination) ), switchMap(([, pagination]) .> { return this.musiciansService.getMusicians(pagination).pipe( ** **. */ ); }) ); });
  79. readonly invokeLoadMusicians$ = createEffect(() .> { return this.actions$.pipe( ofType( musiciansPageActions.currentPageChanged,

    musiciansPageActions.pageSizeChanged ), map(() .> musiciansActions.loadMusicians()) ); }); readonly loadMusicians$ = createEffect(() .> { return this.actions$.pipe( ofType(musiciansActions.loadMusicians), concatLatestFrom(() .> this.store.select(selectMusiciansPagination) ), switchMap(([, pagination]) .> { return this.musiciansService.getMusicians(pagination).pipe( ** **. */ ); }) ); });
  80. readonly invokeLoadMusicians$ = createEffect(() .> { return this.actions$.pipe( ofType( musiciansPageActions.currentPageChanged,

    musiciansPageActions.pageSizeChanged ), map(() .> musiciansActions.loadMusicians()) ); }); readonly loadMusicians$ = createEffect(() .> { return this.actions$.pipe( concatLatestFrom(() .> this.store.select(selectMusiciansPagination) ), switchMap(([, pagination]) .> { return this.musiciansService.getMusicians(pagination).pipe( ** **. */ ); }) ); });
  81. readonly loadMusicians$ = createEffect(() .> { return this.actions$.pipe( ofType( musiciansPageActions.currentPageChanged,

    musiciansPageActions.pageSizeChanged ), concatLatestFrom(() .> this.store.select(selectMusiciansPagination) ), switchMap(([, pagination]) .> { return this.musiciansService.getMusicians(pagination).pipe( ** **. */ ); }) ); });
  82. ★ Name effects like functions ★ Keep effects simple ★

    Don’t create “boiler” effects ★ Apply good action hygiene Effects Tips
  83. readonly loadAlbum$ = createEffect(() .> { return this.actions$.pipe( ofType(albumDetailsPageActions.opened), concatLatestFrom(()

    .> this.store.select(selectAlbumIdFromRoute)), concatMap(([, albumId]) .> { return this.albumsService.getAlbum(albumId).pipe( mergeMap(({ songs, composers }) .> [ songsActions.loadSongsSuccess({ songs }), composersActions.loadComposersSuccess({ composers }), ]), catchError(** **. */) ); }) ); });
  84. readonly loadAlbum$ = createEffect(() .> { return this.actions$.pipe( ofType(albumDetailsPageActions.opened), concatLatestFrom(()

    .> this.store.select(selectAlbumIdFromRoute)), concatMap(([, albumId]) .> { return this.albumsService.getAlbum(albumId).pipe( mergeMap(({ songs, composers }) .> [ songsActions.loadSongsSuccess({ songs }), composersActions.loadComposersSuccess({ composers }), ]), catchError(** **. */) ); }) ); });
  85. readonly loadAlbum$ = createEffect(() .> { return this.actions$.pipe( ofType(albumDetailsPageActions.opened), concatLatestFrom(()

    .> this.store.select(selectAlbumIdFromRoute)), concatMap(([, albumId]) .> { return this.albumsService.getAlbum(albumId).pipe( mergeMap(({ songs, composers }) .> [ songsActions.loadSongsSuccess({ songs }), composersActions.loadComposersSuccess({ composers }), ]), catchError(** **. */) ); }) ); });
  86. readonly loadAlbum$ = createEffect(() .> { return this.actions$.pipe( ofType(albumDetailsPageActions.opened), concatLatestFrom(()

    .> this.store.select(selectAlbumIdFromRoute)), concatMap(([, albumId]) .> { return this.albumsService.getAlbum(albumId).pipe( mergeMap(({ songs, composers }) .> [ songsActions.loadSongsSuccess({ songs }), composersActions.loadComposersSuccess({ composers }), ]), catchError(** **. */) ); }) ); });
  87. readonly loadAlbum$ = createEffect(() .> { return this.actions$.pipe( ofType(albumDetailsPageActions.opened), concatLatestFrom(()

    .> this.store.select(selectAlbumIdFromRoute)), concatMap(([, albumId]) .> { return this.albumsService.getAlbum(albumId).pipe( map(({ songs, composers }) .> albumsApiActions.albumLoadedSuccess({ songs, composers }), ), catchError(** **. */) ); }) ); });
  88. export const songsReducer = createReducer( on(albumsApiActions.albumLoadedSuccess, (state, { songs })

    .> ({ ...state, songs, })) ); export const composersReducer = createReducer( on(albumsApiActions.albumLoadedSuccess, (state, { composers }) .> ({ ...state, composers, })) );
  89. ★ Name effects like functions ★ Keep effects simple ★

    Don’t create “boiler” effects ★ Apply good action hygiene Effects Tips