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

NgRx Store - Tips For Better Code Hygiene

NgRx Store - Tips For Better Code Hygiene

Marko Stanimirović

January 30, 2022
Tweet

More Decks by Marko Stanimirović

Other Decks in Programming

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 ★ Create reusable reducers ★ Treat actions as unique events ★ Group actions by source ★ Don’t dispatch actions conditionally Store Tips
  3. ★ Put global state in a single place ★ Use

    selectors for derived state ★ Create reusable reducers ★ Treat actions as unique events ★ Group actions by source ★ Don’t dispatch actions conditionally 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 ★ Create reusable reducers ★ Treat actions as unique events ★ Group actions by source ★ Don’t dispatch actions conditionally 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 ★ Create reusable reducers ★ Treat actions as unique events ★ Group actions by source ★ Don’t dispatch actions conditionally Store Tips
  23. 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 } ) );
  24. 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 } ) );
  25. 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 } ) );
  26. ★ Put global state in a single place ★ Use

    selectors for derived state ★ Create reusable reducers ★ Treat actions as unique events ★ Group actions by source ★ Don’t dispatch actions conditionally Store Tips
  27. @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' }); } }
  28. @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' }); } }
  29. @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' }); } }
  30. @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' }); } }
  31. @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' }); } }
  32. @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
  33. [Login Page] Login Form Submitted [Auth API] User Logged in

    Successfully [Songs Page] Opened [Songs API] Songs Loaded Successfully [Composers API] Composers Loaded Successfully
  34. [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
  35. ★ Put global state in a single place ★ Use

    selectors for derived state ★ Create reusable reducers ★ Treat actions as unique events ★ Group actions by source ★ Don’t dispatch actions conditionally Store Tips
  36. 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 }>() );
  37. 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 }>() );
  38. 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 }>() );
  39. ★ Put global state in a single place ★ Use

    selectors for derived state ★ Create reusable reducers ★ Treat actions as unique events ★ Group actions by source ★ Don’t dispatch actions conditionally Store Tips
  40. @Component(** **. */) export class SongsComponent implements OnInit { constructor(private

    readonly store: Store) {} ngOnInit(): void { this.store.select(selectSongs).pipe( tap((songs) => { if (!songs) { this.store.dispatch(songsActions.loadSongs()); } }), take(1) ).subscribe(); } }
  41. @Component(** **. */) export class SongsComponent implements OnInit { constructor(private

    readonly store: Store) {} ngOnInit(): void { this.store.select(selectSongs).pipe( tap((songs) => { if (!songs) { this.store.dispatch(songsActions.loadSongs()); } }), take(1) ).subscribe(); } }
  42. readonly loadSongsIfNotLoaded$ = createEffect(() => { return this.actions$.pipe( ofType(songsPageActions.opened), concatLatestFrom(()

    => this.store.select(selectSongs)), filter(([, songs]) => !songs), exhaustMap(() => { return this.songsService.getSongs().pipe( map((songs) => songsApiActions.songsLoadedSuccess({ songs })), catchError((error: { message: string }) => of(songsApiActions.songsLoadedFailure({ error })) ) ); }) ); });
  43. readonly loadSongsIfNotLoaded$ = createEffect(() => { return this.actions$.pipe( ofType(songsPageActions.opened), concatLatestFrom(()

    => this.store.select(selectSongs)), filter(([, songs]) => !songs), exhaustMap(() => { return this.songsService.getSongs().pipe( map((songs) => songsApiActions.songsLoadedSuccess({ songs })), catchError((error: { message: string }) => of(songsApiActions.songsLoadedFailure({ error })) ) ); }) ); });
  44. readonly loadSongsIfNotLoaded$ = createEffect(() => { return this.actions$.pipe( ofType(songsPageActions.opened), concatLatestFrom(()

    => this.store.select(selectSongs)), filter(([, songs]) => !songs), exhaustMap(() => { return this.songsService.getSongs().pipe( map((songs) => songsApiActions.songsLoadedSuccess({ songs })), catchError((error: { message: string }) => of(songsApiActions.songsLoadedFailure({ error })) ) ); }) ); });
  45. readonly loadSongsIfNotLoaded$ = createEffect(() => { return this.actions$.pipe( ofType(songsPageActions.opened), concatLatestFrom(()

    => this.store.select(selectSongs)), filter(([, songs]) => !songs), exhaustMap(() => { return this.songsService.getSongs().pipe( map((songs) => songsApiActions.songsLoadedSuccess({ songs })), catchError((error: { message: string }) => of(songsApiActions.songsLoadedFailure({ error })) ) ); }) ); });
  46. @Component(** **. */) export class SongsComponent implements OnInit { constructor(private

    readonly store: Store) {} ngOnInit(): void { this.store.dispatch(songsPageActions.opened()); } }
  47. ★ Put global state in a single place ★ Use

    selectors for derived state ★ Create reusable reducers ★ Treat actions as unique events ★ Group actions by source ★ Don’t dispatch actions conditionally Store Tips