SlideShare a Scribd company logo
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 ๏ฌle 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!

More Related Content

PDF
What is systemd? Why use it? how does it work? - breizhcamp
PPTX
What is systemd? Why use it? how does it work? - devoxx france 2017
PPT
Perl่ฐƒ็”จๅพฎๅšAPIๅฎž็Žฐ่‡ชๅŠจๆŸฅ่ฏขๅบ”็ญ”
PPTX
Break through the serverless barriers with Durable Functions
PDF
Ngrx slides
PDF
Angular2 & ngrx/store: Game of States
PDF
Gerenciamento de estado no Angular com NgRx
What is systemd? Why use it? how does it work? - breizhcamp
What is systemd? Why use it? how does it work? - devoxx france 2017
Perl่ฐƒ็”จๅพฎๅšAPIๅฎž็Žฐ่‡ชๅŠจๆŸฅ่ฏขๅบ”็ญ”
Break through the serverless barriers with Durable Functions
Ngrx slides
Angular2 & ngrx/store: Game of States
Gerenciamento de estado no Angular com NgRx

Similar to NgRx Store - Tips for Better Code Hygiene (18)

PDF
Evan Schultz - Angular Camp - ng2-redux
PDF
Evan Schultz - Angular Summit - 2016
PPTX
๐€๐ง ๐ˆ๐ง๐ญ๐ž๐ซ๐š๐œ๐ญ๐ข๐ฏ๐ž ๐’๐ž๐ฌ๐ฌ๐ข๐จ๐ง ๐จ๐ง ๐€๐ง๐ ๐ฎ๐ฅ๐š๐ซ ๐๐ ๐‘๐ฑ ๐›๐ฒ ๐€๐ง๐ ๐ฎ๐ฅ๐š๐ซ ๐ƒ๐ž๐ฏ๐ž๐ฅ๐จ๐ฉ๐ฆ๐ž๐ง๐ญ ๐“๐ž๐š๐ฆ
PDF
Getting Started with NgRx (Redux) Angular
PDF
Angular state Management-NgRx
PDF
Higher-Order Components โ€” Ilya Gelman
ย 
PPTX
Best Practices NgRx for Scaling Your Angular Application
PDF
Level up your NgRx game
PDF
Redux Deep Dive - ReactFoo Pune 2018
PDF
Redux data flow with angular
PDF
Redux with angular 2 - workshop 2016
PDF
Introduction to React & Redux
PDF
Effective Application State Management (@DevCamp2017)
PDF
Introduction to Redux (for Angular and React devs)
PDF
angular fundamentals.pdf angular fundamentals.pdf
PDF
angular fundamentals.pdf
PDF
Angular redux
PDF
Dumb and smart components + redux (1)
Evan Schultz - Angular Camp - ng2-redux
Evan Schultz - Angular Summit - 2016
๐€๐ง ๐ˆ๐ง๐ญ๐ž๐ซ๐š๐œ๐ญ๐ข๐ฏ๐ž ๐’๐ž๐ฌ๐ฌ๐ข๐จ๐ง ๐จ๐ง ๐€๐ง๐ ๐ฎ๐ฅ๐š๐ซ ๐๐ ๐‘๐ฑ ๐›๐ฒ ๐€๐ง๐ ๐ฎ๐ฅ๐š๐ซ ๐ƒ๐ž๐ฏ๐ž๐ฅ๐จ๐ฉ๐ฆ๐ž๐ง๐ญ ๐“๐ž๐š๐ฆ
Getting Started with NgRx (Redux) Angular
Angular state Management-NgRx
Higher-Order Components โ€” Ilya Gelman
ย 
Best Practices NgRx for Scaling Your Angular Application
Level up your NgRx game
Redux Deep Dive - ReactFoo Pune 2018
Redux data flow with angular
Redux with angular 2 - workshop 2016
Introduction to React & Redux
Effective Application State Management (@DevCamp2017)
Introduction to Redux (for Angular and React devs)
angular fundamentals.pdf angular fundamentals.pdf
angular fundamentals.pdf
Angular redux
Dumb and smart components + redux (1)
Ad

Recently uploaded (20)

PDF
Digital Strategies for Manufacturing Companies
PPTX
Introduction to Artificial Intelligence
PDF
Addressing The Cult of Project Management Tools-Why Disconnected Work is Hold...
PPTX
Operating system designcfffgfgggggggvggggggggg
PDF
2025 Textile ERP Trends: SAP, Odoo & Oracle
PDF
Wondershare Filmora 15 Crack With Activation Key [2025
PDF
Understanding Forklifts - TECH EHS Solution
PDF
Design an Analysis of Algorithms I-SECS-1021-03
PPTX
Lecture 3: Operating Systems Introduction to Computer Hardware Systems
ย 
PDF
System and Network Administraation Chapter 3
PDF
Flood Susceptibility Mapping Using Image-Based 2D-CNN Deep Learnin. Overview ...
PDF
medical staffing services at VALiNTRY
PDF
Internet Downloader Manager (IDM) Crack 6.42 Build 42 Updates Latest 2025
PDF
SAP S4 Hana Brochure 3 (PTS SYSTEMS AND SOLUTIONS)
PDF
Audit Checklist Design Aligning with ISO, IATF, and Industry Standards โ€” Omne...
PPTX
ManageIQ - Sprint 268 Review - Slide Deck
PPTX
VVF-Customer-Presentation2025-Ver1.9.pptx
PDF
Design an Analysis of Algorithms II-SECS-1021-03
PDF
How Creative Agencies Leverage Project Management Software.pdf
PDF
Why TechBuilder is the Future of Pickup and Delivery App Development (1).pdf
Digital Strategies for Manufacturing Companies
Introduction to Artificial Intelligence
Addressing The Cult of Project Management Tools-Why Disconnected Work is Hold...
Operating system designcfffgfgggggggvggggggggg
2025 Textile ERP Trends: SAP, Odoo & Oracle
Wondershare Filmora 15 Crack With Activation Key [2025
Understanding Forklifts - TECH EHS Solution
Design an Analysis of Algorithms I-SECS-1021-03
Lecture 3: Operating Systems Introduction to Computer Hardware Systems
ย 
System and Network Administraation Chapter 3
Flood Susceptibility Mapping Using Image-Based 2D-CNN Deep Learnin. Overview ...
medical staffing services at VALiNTRY
Internet Downloader Manager (IDM) Crack 6.42 Build 42 Updates Latest 2025
SAP S4 Hana Brochure 3 (PTS SYSTEMS AND SOLUTIONS)
Audit Checklist Design Aligning with ISO, IATF, and Industry Standards โ€” Omne...
ManageIQ - Sprint 268 Review - Slide Deck
VVF-Customer-Presentation2025-Ver1.9.pptx
Design an Analysis of Algorithms II-SECS-1021-03
How Creative Agencies Leverage Project Management Software.pdf
Why TechBuilder is the Future of Pickup and Delivery App Development (1).pdf
Ad

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
  • 5. โ˜… 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
  • 6. โ˜… 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
  • 7. Keep the NgRx Store as the only source of global state.
  • 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 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
  • 26. Don't put the derived 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 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
  • 35. Case reducers can listen 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 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
  • 43. @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' }); } }
  • 44. @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' }); } }
  • 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] Load Songs' }); this.store.dispatch({ type: '[Composers] Load Composers' }); } }
  • 46. @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' }); } }
  • 47. Don't dispatch multiple actions sequentially.
  • 48. @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' }); } }
  • 49. Be consistent in naming actions. Use "[Source] Event" pattern.
  • 50. @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
  • 51. [Login Page] Login Form Submitted [Auth API] User Logged in Successfully [Songs Page] Opened [Songs API] Songs Loaded Successfully [Composers API] Composers Loaded Successfully
  • 52. [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
  • 53. โ˜… 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
  • 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 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
  • 60. Don't dispatch actions conditionally based on the state value.
  • 61. @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(); } }
  • 62. @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(); } }
  • 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(** **. */) export class SongsComponent implements OnInit { constructor(private readonly store: Store) {} ngOnInit(): void { this.store.dispatch(songsPageActions.opened()); } }
  • 68. โ˜… 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