NgRx Store - Tips for Better Code Hygiene
Marko Stanimirović
Marko Stanimirović
@MarkoStDev
★ Sr. Frontend Engineer at JobCloud
★ NgRx Team Member
★ Angular Belgrade Organizer
★ Hobby Musician
★ M.Sc. in Software Engineering
@ngrx/effects
@ngrx/store
@ngrx/effects
@ngrx/store
★ 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
★ 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
Keep the NgRx Store as the only source of global state.
SongsComponent
songsWithComposers$ =
SongsService
songs$: Observable<Song[]>;
activeSong$: Observable<Song | null>;
SongsComponent
songsWithComposers$ =
SongsService
songs$: Observable<Song[]>;
activeSong$: Observable<Song | null>;
SongsComponent
songsWithComposers$ =
NgRx Store
composers: {
entities: Dictionary<Composer>;
};
SongsService
songs$: Observable<Song[]>;
activeSong$: Observable<Song | null>;
SongsComponent
songsWithComposers$ = combineLatest([
])
NgRx Store
composers: {
entities: Dictionary<Composer>;
};
SongsService
songs$: Observable<Song[]>;
activeSong$: Observable<Song | null>;
SongsComponent
songsWithComposers$ = combineLatest([
this.songsService.songs$,
])
NgRx Store
composers: {
entities: Dictionary<Composer>;
};
SongsService
songs$: Observable<Song[]>;
activeSong$: Observable<Song | null>;
SongsComponent
songsWithComposers$ = combineLatest([
this.songsService.songs$,
this.store.select(selectComposers),
])
NgRx Store
composers: {
entities: Dictionary<Composer>;
};
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>;
};
SongsService
songs$: Observable<Song[]>;
activeSong$: Observable<Song | null>;
SongsFacade
songsWithComposers$ =
NgRx Store
composers: {
entities: Dictionary<Composer>;
};
SongsComponent
songsWithComposers$ =
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$ =
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$;
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$;
NgRx Store
songs: {
entities: Dictionary<Song>;
activeId: string | null;
};
composers: {
entities: Dictionary<Composer>;
};
NgRx Store
songs: {
entities: Dictionary<Song>;
activeId: string | null;
};
composers: {
entities: Dictionary<Composer>;
};
songs.selectors.ts
const selectSongsWithComposers =
NgRx Store
songs: {
entities: Dictionary<Song>;
activeId: string | null;
};
composers: {
entities: Dictionary<Composer>;
};
songs.selectors.ts
const selectSongsWithComposers = createSelector(
selectAllSongs,
);
NgRx Store
songs: {
entities: Dictionary<Song>;
activeId: string | null;
};
composers: {
entities: Dictionary<Composer>;
};
songs.selectors.ts
const selectSongsWithComposers = createSelector(
selectAllSongs,
selectComposers,
);
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],
}))
);
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);
★ 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
Don't put the derived state in the store.
export const musiciansReducer = createReducer(
on(musiciansPageActions.search, (state, { query }) => {
const filteredMusicians = state.musicians.filter(({ name }) =>
name.includes(query)
);
return {
==.state,
query,
filteredMusicians,
};
})
);
export const musiciansReducer = createReducer(
on(musiciansPageActions.search, (state, { query }) => {
const filteredMusicians = state.musicians.filter(({ name }) =>
name.includes(query)
);
return {
==.state,
query,
filteredMusicians,
};
})
);
export const selectFilteredMusicians = createSelector(
selectAllMusicians,
selectMusicianQuery,
(musicians, query) =>
musicians.filter(({ name }) => name.includes(query))
);
export const selectFilteredMusicians = createSelector(
selectAllMusicians,
selectMusicianQuery,
(musicians, query) =>
musicians.filter(({ name }) => name.includes(query))
);
export const selectFilteredMusicians = createSelector(
selectAllMusicians,
selectMusicianQuery,
(musicians, query) =>
musicians.filter(({ name }) => name.includes(query))
);
export const selectFilteredMusicians = createSelector(
selectAllMusicians,
selectMusicianQuery,
(musicians, query) =>
musicians.filter(({ name }) => name.includes(query))
);
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,
}))
);
★ 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
Case reducers can listen to multiple actions.
export const composersReducer = createReducer(
initialState,
on(
composerExistsGuardActions.canActivate,
composersPageActions.opened,
songsPageActions.opened,
(state) => ({ ==.state, isLoading: true })
)
);
export const composersReducer = createReducer(
initialState,
on(
composerExistsGuardActions.canActivate,
composersPageActions.opened,
songsPageActions.opened,
(state) => ({ ==.state, isLoading: true })
)
);
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 }
)
);
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 }
)
);
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 }
)
);
★ 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
Don’t treat actions as commands.
@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' });
}
}
@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' });
}
}
@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' });
}
}
@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' });
}
}
Don't dispatch multiple actions sequentially.
@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' });
}
}
Be consistent in naming actions. Use "[Source] Event" pattern.
@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
[Login Page] Login Form Submitted
[Auth API] User Logged in Successfully
[Songs Page] Opened
[Songs API] Songs Loaded Successfully
[Composers API] Composers Loaded Successfully
[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
★ 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
Create action file by source.
songs-page.actions.ts
export const opened = createAction('[Songs Page] Opened');
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-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 }>()
);
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 }>()
);
★ 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
Don't dispatch actions conditionally based on the state value.
@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();
}
}
@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();
}
}
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 }))
)
);
})
);
});
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 }))
)
);
})
);
});
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 }))
)
);
})
);
});
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 }))
)
);
})
);
});
@Component(** **. */)
export class SongsComponent implements OnInit {
constructor(private readonly store: Store) {}
ngOnInit(): void {
this.store.dispatch(songsPageActions.opened());
}
}
★ 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
Marko Stanimirović
@MarkoStDev
Thank You!

NgRx Store - Tips for Better Code Hygiene

  • 1.
    NgRx Store -Tips for Better Code Hygiene Marko Stanimirović
  • 2.
    Marko Stanimirović @MarkoStDev ★ Sr.Frontend Engineer at JobCloud ★ NgRx Team Member ★ Angular Belgrade Organizer ★ Hobby Musician ★ M.Sc. in Software Engineering
  • 3.
  • 4.
  • 5.
    ★ Put globalstate 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
  • 6.
    ★ Put globalstate 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
  • 7.
    Keep the NgRxStore as the only source of global state.
  • 8.
  • 9.
    SongsService songs$: Observable<Song[]>; activeSong$: Observable<Song| null>; SongsComponent songsWithComposers$ =
  • 10.
    SongsService songs$: Observable<Song[]>; activeSong$: Observable<Song| null>; SongsComponent songsWithComposers$ = NgRx Store composers: { entities: Dictionary<Composer>; };
  • 11.
    SongsService songs$: Observable<Song[]>; activeSong$: Observable<Song| null>; SongsComponent songsWithComposers$ = combineLatest([ ]) NgRx Store composers: { entities: Dictionary<Composer>; };
  • 12.
    SongsService songs$: Observable<Song[]>; activeSong$: Observable<Song| null>; SongsComponent songsWithComposers$ = combineLatest([ this.songsService.songs$, ]) NgRx Store composers: { entities: Dictionary<Composer>; };
  • 13.
    SongsService songs$: Observable<Song[]>; activeSong$: Observable<Song| null>; SongsComponent songsWithComposers$ = combineLatest([ this.songsService.songs$, this.store.select(selectComposers), ]) NgRx Store composers: { entities: Dictionary<Composer>; };
  • 14.
    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>; };
  • 15.
    SongsService songs$: Observable<Song[]>; activeSong$: Observable<Song| null>; SongsFacade songsWithComposers$ = NgRx Store composers: { entities: Dictionary<Composer>; }; SongsComponent songsWithComposers$ =
  • 16.
    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$ =
  • 17.
    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$;
  • 18.
    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$;
  • 19.
    NgRx Store songs: { entities:Dictionary<Song>; activeId: string | null; }; composers: { entities: Dictionary<Composer>; };
  • 20.
    NgRx Store songs: { entities:Dictionary<Song>; activeId: string | null; }; composers: { entities: Dictionary<Composer>; }; songs.selectors.ts const selectSongsWithComposers =
  • 21.
    NgRx Store songs: { entities:Dictionary<Song>; activeId: string | null; }; composers: { entities: Dictionary<Composer>; }; songs.selectors.ts const selectSongsWithComposers = createSelector( selectAllSongs, );
  • 22.
    NgRx Store songs: { entities:Dictionary<Song>; activeId: string | null; }; composers: { entities: Dictionary<Composer>; }; songs.selectors.ts const selectSongsWithComposers = createSelector( selectAllSongs, selectComposers, );
  • 23.
    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], })) );
  • 24.
    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);
  • 25.
    ★ Put globalstate 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
  • 26.
    Don't put thederived state in the store.
  • 27.
    export const musiciansReducer= createReducer( on(musiciansPageActions.search, (state, { query }) => { const filteredMusicians = state.musicians.filter(({ name }) => name.includes(query) ); return { ==.state, query, filteredMusicians, }; }) );
  • 28.
    export const musiciansReducer= createReducer( on(musiciansPageActions.search, (state, { query }) => { const filteredMusicians = state.musicians.filter(({ name }) => name.includes(query) ); return { ==.state, query, filteredMusicians, }; }) );
  • 29.
    export const selectFilteredMusicians= createSelector( selectAllMusicians, selectMusicianQuery, (musicians, query) => musicians.filter(({ name }) => name.includes(query)) );
  • 30.
    export const selectFilteredMusicians= createSelector( selectAllMusicians, selectMusicianQuery, (musicians, query) => musicians.filter(({ name }) => name.includes(query)) );
  • 31.
    export const selectFilteredMusicians= createSelector( selectAllMusicians, selectMusicianQuery, (musicians, query) => musicians.filter(({ name }) => name.includes(query)) );
  • 32.
    export const selectFilteredMusicians= createSelector( selectAllMusicians, selectMusicianQuery, (musicians, query) => musicians.filter(({ name }) => name.includes(query)) );
  • 33.
    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, })) );
  • 34.
    ★ Put globalstate 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
  • 35.
    Case reducers canlisten to multiple actions.
  • 36.
    export const composersReducer= createReducer( initialState, on( composerExistsGuardActions.canActivate, composersPageActions.opened, songsPageActions.opened, (state) => ({ ==.state, isLoading: true }) ) );
  • 37.
    export const composersReducer= createReducer( initialState, on( composerExistsGuardActions.canActivate, composersPageActions.opened, songsPageActions.opened, (state) => ({ ==.state, isLoading: true }) ) );
  • 38.
    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 } ) );
  • 39.
    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 } ) );
  • 40.
    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 } ) );
  • 41.
    ★ Put globalstate 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
  • 42.
  • 43.
    @Component(** **. */) exportclass SongsComponent implements OnInit { readonly songs$ = this.store.select(selectSongs); constructor(private readonly store: Store) {} ngOnInit(): void { this.store.dispatch({ type: '[Songs] Load Songs' }); } }
  • 44.
    @Component(** **. */) exportclass SongsComponent implements OnInit { readonly songs$ = this.store.select(selectSongs); constructor(private readonly store: Store) {} ngOnInit(): void { this.store.dispatch({ type: '[Songs] Load Songs' }); } }
  • 45.
    @Component(** **. */) exportclass 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' }); } }
  • 46.
    @Component(** **. */) exportclass 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' }); } }
  • 47.
    Don't dispatch multipleactions sequentially.
  • 48.
    @Component(** **. */) exportclass SongsComponent implements OnInit { readonly songs$ = this.store.select(selectSongsWithComposers); constructor(private readonly store: Store) {} ngOnInit(): void { this.store.dispatch({ type: '[Songs Page] Opened' }); } }
  • 49.
    Be consistent innaming actions. Use "[Source] Event" pattern.
  • 50.
    @Component(** **. */) exportclass 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
  • 51.
    [Login Page] LoginForm Submitted [Auth API] User Logged in Successfully [Songs Page] Opened [Songs API] Songs Loaded Successfully [Composers API] Composers Loaded Successfully
  • 52.
    [Login Page] LoginForm 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
  • 53.
    ★ Put globalstate 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
  • 54.
  • 55.
    songs-page.actions.ts export const opened= createAction('[Songs Page] Opened');
  • 56.
    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 }>() );
  • 57.
    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 }>() );
  • 58.
    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 }>() );
  • 59.
    ★ Put globalstate 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
  • 60.
    Don't dispatch actionsconditionally based on the state value.
  • 61.
    @Component(** **. */) exportclass 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(); } }
  • 62.
    @Component(** **. */) exportclass 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(); } }
  • 63.
    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 })) ) ); }) ); });
  • 64.
    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 })) ) ); }) ); });
  • 65.
    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 })) ) ); }) ); });
  • 66.
    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 })) ) ); }) ); });
  • 67.
    @Component(** **. */) exportclass SongsComponent implements OnInit { constructor(private readonly store: Store) {} ngOnInit(): void { this.store.dispatch(songsPageActions.opened()); } }
  • 68.
    ★ Put globalstate 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
  • 69.