Slide 1

Slide 1 text

NgRx Tips for Future-Proof Angular Apps Marko Stanimirović

Slide 2

Slide 2 text

Marko Stanimirović @MarkoStDev ★ Sr. Frontend Engineer at JobCloud ★ NgRx Team Member ★ Angular Belgrade Organizer ★ Hobby Musician ★ M.Sc. in Software Engineering

Slide 3

Slide 3 text

@ngrx/effects @ngrx/store

Slide 4

Slide 4 text

@ngrx/effects @ngrx/store

Slide 5

Slide 5 text

★ 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

Slide 6

Slide 6 text

★ 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

Slide 7

Slide 7 text

Keep the NgRx Store as the only source of global state.

Slide 8

Slide 8 text

SongsComponent songsWithComposers$ =

Slide 9

Slide 9 text

SongsService songs$: Observable; activeSong$: Observable; SongsComponent songsWithComposers$ =

Slide 10

Slide 10 text

SongsService songs$: Observable; activeSong$: Observable; SongsComponent songsWithComposers$ = NgRx Store composers: { entities: Dictionary; };

Slide 11

Slide 11 text

SongsService songs$: Observable; activeSong$: Observable; SongsComponent songsWithComposers$ = combineLatest([ ]) NgRx Store composers: { entities: Dictionary; };

Slide 12

Slide 12 text

SongsService songs$: Observable; activeSong$: Observable; SongsComponent songsWithComposers$ = combineLatest([ this.songsService.songs$, ]) NgRx Store composers: { entities: Dictionary; };

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

SongsService songs$: Observable; activeSong$: Observable; 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; };

Slide 15

Slide 15 text

SongsService songs$: Observable; activeSong$: Observable; SongsFacade songsWithComposers$ = NgRx Store composers: { entities: Dictionary; }; SongsComponent songsWithComposers$ =

Slide 16

Slide 16 text

SongsService songs$: Observable; activeSong$: Observable; 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; }; SongsComponent songsWithComposers$ =

Slide 17

Slide 17 text

SongsService songs$: Observable; activeSong$: Observable; 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; }; SongsComponent songsWithComposers$ = this.facade.songsWithComposers$;

Slide 18

Slide 18 text

SongsService songs$: Observable; activeSong$: Observable; 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; }; SongsComponent songsWithComposers$ = this.facade.songsWithComposers$;

Slide 19

Slide 19 text

NgRx Store songs: { entities: Dictionary; activeId: string | null; }; composers: { entities: Dictionary; };

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

NgRx Store songs: { entities: Dictionary; activeId: string | null; }; composers: { entities: Dictionary; }; songs.selectors.ts const selectSongsWithComposers = createSelector( selectAllSongs, selectComposers, (songs, composers) .> songs.map((song) .> ({ ...song, composer: composers[song.composerId], })) ); SongsComponent songsWithComposers$ = this.store.select(selectSongsWithComposers);

Slide 25

Slide 25 text

★ 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

Slide 26

Slide 26 text

Don't put the derived state in the store.

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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, })) );

Slide 34

Slide 34 text

★ 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

Slide 35

Slide 35 text

Make container components simpler.

Slide 36

Slide 36 text

musicians.component.ts @Component({ selector: 'musicians', template: ` ./musician-search> ./musician-list> ./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) {} }

Slide 37

Slide 37 text

musicians.component.ts @Component({ selector: 'musicians', template: ` ./musician-search> ./musician-list> ./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) {} }

Slide 38

Slide 38 text

musicians.component.ts @Component({ selector: 'musicians', template: ` ./musician-search> ./musician-list> ./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) {} }

Slide 39

Slide 39 text

musicians.component.ts @Component({ selector: 'musicians', template: ` ./musician-search> ./musician-list> ./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) {} }

Slide 40

Slide 40 text

musicians.component.ts @Component({ selector: 'musicians', template: ` ./musician-search> ./musician-list> ./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( );

Slide 41

Slide 41 text

musicians.component.ts @Component({ selector: 'musicians', template: ` ./musician-search> ./musician-list> ./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, );

Slide 42

Slide 42 text

musicians.component.ts @Component({ selector: 'musicians', template: ` ./musician-search> ./musician-list> ./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, );

Slide 43

Slide 43 text

musicians.component.ts @Component({ selector: 'musicians', template: ` ./musician-search> ./musician-list> ./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, );

Slide 44

Slide 44 text

musicians.component.ts @Component({ selector: 'musicians', template: ` ./musician-search> ./musician-list> ./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, }) );

Slide 45

Slide 45 text

musicians.component.ts @Component({ selector: 'musicians', template: ` ./musician-search> ./musician-list> ./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, }) );

Slide 46

Slide 46 text

musicians.component.ts @Component({ selector: 'musicians', template: ` ./musician-search> ./musician-list> ./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, }) );

Slide 47

Slide 47 text

musicians.component.ts @Component({ selector: 'musicians', template: ` ./musician-search> ./musician-list> ./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, }) );

Slide 48

Slide 48 text

★ 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

Slide 49

Slide 49 text

Case reducers can listen to multiple actions.

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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 } ) );

Slide 53

Slide 53 text

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 } ) );

Slide 54

Slide 54 text

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 } ) );

Slide 55

Slide 55 text

★ 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

Slide 56

Slide 56 text

Don’t treat actions as commands.

Slide 57

Slide 57 text

@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' }); } }

Slide 58

Slide 58 text

@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' }); } }

Slide 59

Slide 59 text

@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' }); } }

Slide 60

Slide 60 text

@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' }); } }

Slide 61

Slide 61 text

Don't dispatch multiple actions sequentially.

Slide 62

Slide 62 text

@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' }); } }

Slide 63

Slide 63 text

Be consistent in naming actions. Use "[Source] Event" pattern.

Slide 64

Slide 64 text

@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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

[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

Slide 67

Slide 67 text

★ 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

Slide 68

Slide 68 text

Create action file by source.

Slide 69

Slide 69 text

songs-page.actions.ts export const opened = createAction('[Songs Page] Opened');

Slide 70

Slide 70 text

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 }>() );

Slide 71

Slide 71 text

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 }>() );

Slide 72

Slide 72 text

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 }>() );

Slide 73

Slide 73 text

★ 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

Slide 74

Slide 74 text

@ngrx/effects @ngrx/store

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

Don’t name effects based on the action they are listening to.

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

Move the business logic to services.

Slide 85

Slide 85 text

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 })) ) ); }) ); });

Slide 86

Slide 86 text

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 })) ) ); }) ); });

Slide 87

Slide 87 text

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 })) ) ); }) ); });

Slide 88

Slide 88 text

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 })) ) ); }) ); });

Slide 89

Slide 89 text

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 })) ) ); }) ); });

Slide 90

Slide 90 text

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 })) ) ); }) ); });

Slide 91

Slide 91 text

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 })) ) ); }) ); });

Slide 92

Slide 92 text

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 })) ) ); }) ); });

Slide 93

Slide 93 text

@Injectable({ providedIn: 'root' }) export class MusiciansService { }

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

@Injectable({ providedIn: 'root' }) export class MusiciansService { constructor( private readonly musiciansResource: MusiciansResource, private readonly bandsResource: BandsResource ) {} getMusician(musicianId: string): Observable { return this.musiciansResource.getMusician(musicianId).pipe( mergeMap((musician) .> { return this.bandsResource.getBand(musician.bandId).pipe( map((band) .> ({ ...musician, bandName: band.name })) ); }) ); } }

Slide 96

Slide 96 text

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 })) ) ); }) ); });

Slide 97

Slide 97 text

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 })) ) ); }) ); });

Slide 98

Slide 98 text

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 })) ) ); }) ); });

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

NgRx effect can listen to multiple events.

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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( ** **. */ ); }) ); });

Slide 105

Slide 105 text

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( ** **. */ ); }) ); });

Slide 106

Slide 106 text

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( ** **. */ ); }) ); });

Slide 107

Slide 107 text

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( ** **. */ ); }) ); });

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

Don’t return an array of actions (commands) from the effect.

Slide 110

Slide 110 text

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(** **. */) ); }) ); });

Slide 111

Slide 111 text

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(** **. */) ); }) ); });

Slide 112

Slide 112 text

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(** **. */) ); }) ); });

Slide 113

Slide 113 text

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(** **. */) ); }) ); });

Slide 114

Slide 114 text

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(** **. */) ); }) ); });

Slide 115

Slide 115 text

export const songsReducer = createReducer( on(albumsApiActions.albumLoadedSuccess, (state, { songs }) .> ({ ...state, songs, })) ); export const composersReducer = createReducer( on(albumsApiActions.albumLoadedSuccess, (state, { composers }) .> ({ ...state, composers, })) );

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

Marko Stanimirović @MarkoStDev Thank You!