NG-RX
STATE MANAGEMENT IN ANGULAR
ABOUT ME
SUWIGYA RATHORE
▸ Front end developer with 7+ years of experience
▸ Working for Non Dutch from Sep 2018
▸ Working on Angular from starting almost 2+ years
▸ Have experience of 2 years with largest customer facing Angular website Air
France & KLM (probably Air Kenya, Joon, Transavia etc.)
▸ Working with dffrntmedia on Earthtoday.com
▸ Loves to travel
▸ Read and experiment about latest technologies
▸ Loves to do binge watching on Netflix
RULE 4: REDUX SHOULD THE MEANS OF
ACHIEVING A GOAL, NOT THE GOAL
RULE 5: ALWAYS TREAT ROUTER AS THE SOURCE
OF TRUTH
Victor Savkin
NG-RX
https://blog.nrwl.io/managing-state-in-angular-applications-22b75ef5625f
3
GOLDEN RULES
TEXT
AGENDA
▸ State and reducer setup
▸ NG-RX Store setup @ngrx/store
▸ Accessing State in Component
▸ Redux devtools with angular @ngrx/store-devtools
▸ Collection management @ngrx/entity
▸ Dispatching strongly typed actions
▸ State selectors
▸ Async operations with effects @ngrx/effects
▸ Improve server communication with NX data persistence layer @nrwl/nx
▸ Connect Related data models
▸ Folder structure in project & demo
4
NG-RX
export interface FeatureAState {
projects: Project[],
selectedProjectId: string | null;
}
export const initialFeatureAState: FeatureAState = {
projects: initialProjects,
selectedProjectId: null
}
export function featureAReducer(
state = initialFeatureAState, action): FeatureAState{
switch(action.type) {
default:
return state;
}
}
)
CREATING INTERFACE FOR FEATURE STATE
5
STATE AND REDUCER
CREATING AN INITIAL STATE
SETTING UP BASIC REDUCER
* Here Project is class of type project
NG-RX
import * as fromFeatureA from ‘./featureA.reducer';
export interface AppState {
featureA: fromFeatureA.FeatureAState,
featureB: fromFeatureB.FeatureBState
}
export const reducers: ActionReducerMap<AppState> = {
featureA: fromFeatureA.FeatureAReducer,
featureB: fromFeatureB.FeatureBReducer
}
IMPORTING EVERYTHING FROM FEATURE
6
STORE AND MODULE
MERGING FEATURE STATE IN APP STATE
MERGING FEATURE REDUCER IN APP REDUCER
import { StoreModule } from ‘@ngrx/store’;
import { NgNodule } from '@angular/core';
import { reducers } from ‘.’;
@NgModule({
imports: [StoreModule.forRoot(reducers)]
})
export class StateModule {}
IMPORTING APP REDUCER IN MODULE
IMPORT STORE MODULE AND PASS REDUCERS
import { NgNodule } from '@angular/core';
import { StateModule } from ‘./state.module’;
@NgModule({
imports: [StateModule]
})
export class AppModule {}
IMPORT STATE MODULE IN APP MODULE
NG-RX
import { FeatureAState } from ‘./redux/featureA.reducer’;
constructor(
private store: Store<FeatureAState>
){
this.projects$ = store.pipe(
select(‘featureA’),
map((featureAState: FeatureAState) =>
featureAState.projects)
)
}
7
ACCESSING STORE IN COMPONENT
IMPORT FEATURE STATE
INTERFACE
INJECTING STORE AS SERVICE
SELECTING FIELD FROM FEATURE
STATE
NG-RX
import { StoreModule } from ‘@ngrx/store’;
import { StoreDevToolsModule } from ‘@ngrx/store-devtools';
import { NgNodule } from '@angular/core';
import { reducers } from ‘.’;
@NgModule({
imports: [StoreModule.forRoot(reducers),
StoreDevtoolsModule.instrument({ maxAge: 10 })]
})
export class StateModule {}
8
REDUX DEVTOOLS
IMPORT STORE DEV TOOLS MODULE
NG-RX 9
STRONGLY TYPED ACTIONS
export enum FeatureAActionTypes {
ProjectFetched = '[FeatureA] Fetch’,
ProjectSelected = '[FeatureA] Selected’,
}
import { Action } from '@ngrx/store';
export class SelectProject implements Action {
readonly type = FeatureAActionTypes.ProjectSelected;
constructor(private payload: Project) {}
}
export class FetchProject implements Action {
readonly type = FeatureAActionTypes.ProjectFetched;
}
export type FeatureAAction = SelectProject
| FetchProject
CREATING ENUM FOR ALL ACTIONS
CREATING TYPE OF ACTION WITHOUT PAYLOAD
CREATING TYPE OF ACTION WITH PAYLOAD
EXPORTING ALL ACTIONS AS TYPE
NG-RX 10
MODIFY REDUCER TO USE NEW TYPED ACTIONS
import { FeatureAActionTypes } from ‘./featureA.actions’;
export function featureAReducer(
state = initialFeatureAState, action): FeatureAState{
switch(action.type) {
case FeatureAActionTypes.ProjectSelected:
return {
selectedProjectId: action.payload,
projects: state.projects
}
default:
return state;
}
}
)
IMPORT ACTION ENUM
MAPPING CASES WITH NEW ENUMS
NG-RX 11
COLLECTION MANAGEMENT
export interface FeatureAState extends EntityState<Project> {
selectedProjectId: string | null;
}
export interface FeatureAState {
projects: Project[],
selectedProjectId: string | null;
}
*OLD STATE AS FROM PREVIOUS SLIDE
CHANGING STATE AS ENTITY STATE
export interface EntityState<T> {
ids: string[] | number[];
entities: Dictionary<T>;
}
export const adapter: EntityAdapter<Project> = createEntityAdapter<Project>();
export const initialFeatureAState: FeatureAState = {
projects: initialProjects,
selectedProjectId: null
}
*OLD INITIAL STATE
export const initialState: ProjectsState = adapter.getInitialState({
selectedProjectId: null
}) PULLING INITIAL STATE OUT OF ADAPTER
import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';
NG-RX 12
BENEFITS OF ENTITY COLLECTION
export function featureAReducer(
state = initialFeatureAState, action): featureAState {
switch (action.type) {
case FeatureAActionTypes.ProjectSelected:
return Object.assign({}, state, { selectedProjectId: action.payload });
case FeatureAActionTypes.ProjectFetched:
return adapter.addMany(action.payload, state);
case FeatureAActionTypes.AddProject:
return adapter.addOne(action.payload, state);
case FeatureAActionTypes.UpdateProject:
return adapter.updateOne(action.payload, state);
case FeatureAActionTypes.DeleteProject:
return adapter.removeOne(action.payload, state);
default:
return state;
}
}
NG-RX
ACCESSING ENTITY COLLECTION IN COMPONENT
constructor(
private store: Store<FeatureAState>
){
this.projects$ = store.pipe(
select(‘featureA’),
map((featureAState: FeatureAState) =>
featureAState.projects)
)
}
constructor(
private store: Store<FeatureAState>
){
this.projects$ = store.pipe(
select(‘featureA'),
map(data => data.entities),
map(data => Object.keys(data).map(k => data[k]))
)
}
*OLD WAY ENTITY COLLECTION IN REDUX DEV TOOLS
NEW WAY (ITS BIT COMPLICATED BUT ONLY IN THIS SLIDE)
13
NG-RX
SELECTORS TO MAKE LIFE EASIER
export const getSelectedProjectID = (state: FeatureAState) => state.selectedProjectId;
LOW LEVEL SELECTOR
const { selectIds, selectEntities, selectAll } = adapter.getSelectors();
export const selectProjectIds = selectIds;
export const selectProjectEntities = selectEntities;
export const selectAllProjects = selectAll;
ADAPTER SELECTORS
import { createFeatureSelector, createSelector } from '@ngrx/store'
export const selectFeatureAState
= createFeatureSelector<FeatureAState>(‘featureA');
export const selectProjectIds = createSelector(
selectProjectsState,
fromFeatureA.selectProjectIds
);
export const selectProjectEntities = createSelector(
selectProjectsState,
fromFeatureA.selectProjectEntities
);
export const selectAllProjects = createSelector(
selectProjectsState,
fromFeatureA.selectAllProjects
);
14
NG-RX
ACCESSING STATE BY SELECTOR IN COMPONENT
constructor(
...
this.projects$ = store.pipe(
select(selectAllProjects)
);
constructor(
private store: Store<FeatureAState>
){
this.projects$ = store.pipe(
select(‘featureA'),
map(data => data.entities),
map(data => Object.keys(data).map(k => data[k]))
)
}
FROM THAT SHIT TO THIS BEAUTY
15
NG-RX
EFFECTS TO HANDLE SIDE EFFECTS (ASYNC)
export enum FeatureAActionTypes {
ProjectFetch = '[FeatureA] Fetch’,
ProjectFetched = '[FeatureA] Fetch Success’,
ProjectSelected = '[FeatureA] Selected’,
}
SPLIT ACTIONS INTO STAGES
@Injectable({providedIn: 'root'})
export class FeatureAEffects {
constructor(
private actions$: Actions,
private projectsService: ProjectsService
) { }
@Effect() fetchProjects$ = this.actions$.pipe(
ofType(FeatureAActionTypes.ProjectFetch),
switchMap((action: FetchProject)) =>
this.projectsService.all()
.pipe(map((res: Project[]) => new ProjectFetched))
)
);
TRIGGER EVENT
COMPLETION EVENT
@NgModule({
imports: [
...
EffectsModule.forRoot([
CustomersEffects,
ProjectsEffects
])
]
})
FINALLY IMPORT IT IN MODULE
16
NG-RX
IMPROVE DATA PERSISTENCE IN EFFECTS WITH NX
17
import { DataPersistence } from '@nrwl/nx';
constructor(
private actions$: Actions,
private dataPersistence: DataPersistence<FeatureAState>,
private projectsService: ProjectsService
) { }
}
@Effect() fetchProjects$ = this.dataPersistence.fetch(FeatureAActionTypes.ProjectFetch, {
run: (action: FetchProject, state: FeatureAState) => {
return this.projectsService.all().pipe(map((res: Project[]) => new ProjectFetched(res)))
},
onError: (action: LoadProjects, error) => {
console.log('ERROR', error);
}
});
@Effect() addProjects$ = this.dataPersistence.pessimisticUpdate(ProjectsActionTypes.addProjects, {
run: () => {},
onError: () => {}
});
@Effect() addProjects$ = this.dataPersistence.optimisticUpdate(ProjectsActionTypes.addProjects, {
run: () => {},
onError: () => {}
});
CLEANING UP EFFECTS WITH DATA
PERSISTENCE
PESSIMISTIC UPDATE
OPTIMISTIC UPDATE
https://nrwl.io/nx/guide-data-persistence
NG-RX
HANDLING RELATIONAL MODELS
18
export const selectFeatureAState
= createFeatureSelector<FeatureAState>(‘featureA');
export const selectCurrentProjectId = createSelector(
selectFeatureAState,
fromFeatureA.getSelectedProjectId
);
export const getSelectedProjectID = (state: FeatureAState) => state.selectedProjectId;
export const selectCurrentProject = createSelector(
selectProjectEntities,
selectCurrentProjectId,
(projectEntities, projectId) =>
return projectId ? projectEntities[projectId] : null ;
)
const { selectIds, selectEntities, selectAll } = adapter.getSelectors();
export const selectProjectEntities = selectEntities;
export const selectCustomrsProjects = createSelector(
selectAllCustomers,
selectAllProjects,
(customers, projects) => {}
)
COMPOSING SELECTORS FROM SAME
MODEL
COMPOSING SELECTORS FROM DIFFERENT
MODEL
NG-RX
QUESTIONS

Ngrx

  • 1.
  • 2.
    ABOUT ME SUWIGYA RATHORE ▸Front end developer with 7+ years of experience ▸ Working for Non Dutch from Sep 2018 ▸ Working on Angular from starting almost 2+ years ▸ Have experience of 2 years with largest customer facing Angular website Air France & KLM (probably Air Kenya, Joon, Transavia etc.) ▸ Working with dffrntmedia on Earthtoday.com ▸ Loves to travel ▸ Read and experiment about latest technologies ▸ Loves to do binge watching on Netflix
  • 3.
    RULE 4: REDUXSHOULD THE MEANS OF ACHIEVING A GOAL, NOT THE GOAL RULE 5: ALWAYS TREAT ROUTER AS THE SOURCE OF TRUTH Victor Savkin NG-RX https://blog.nrwl.io/managing-state-in-angular-applications-22b75ef5625f 3 GOLDEN RULES
  • 4.
    TEXT AGENDA ▸ State andreducer setup ▸ NG-RX Store setup @ngrx/store ▸ Accessing State in Component ▸ Redux devtools with angular @ngrx/store-devtools ▸ Collection management @ngrx/entity ▸ Dispatching strongly typed actions ▸ State selectors ▸ Async operations with effects @ngrx/effects ▸ Improve server communication with NX data persistence layer @nrwl/nx ▸ Connect Related data models ▸ Folder structure in project & demo 4
  • 5.
    NG-RX export interface FeatureAState{ projects: Project[], selectedProjectId: string | null; } export const initialFeatureAState: FeatureAState = { projects: initialProjects, selectedProjectId: null } export function featureAReducer( state = initialFeatureAState, action): FeatureAState{ switch(action.type) { default: return state; } } ) CREATING INTERFACE FOR FEATURE STATE 5 STATE AND REDUCER CREATING AN INITIAL STATE SETTING UP BASIC REDUCER * Here Project is class of type project
  • 6.
    NG-RX import * asfromFeatureA from ‘./featureA.reducer'; export interface AppState { featureA: fromFeatureA.FeatureAState, featureB: fromFeatureB.FeatureBState } export const reducers: ActionReducerMap<AppState> = { featureA: fromFeatureA.FeatureAReducer, featureB: fromFeatureB.FeatureBReducer } IMPORTING EVERYTHING FROM FEATURE 6 STORE AND MODULE MERGING FEATURE STATE IN APP STATE MERGING FEATURE REDUCER IN APP REDUCER import { StoreModule } from ‘@ngrx/store’; import { NgNodule } from '@angular/core'; import { reducers } from ‘.’; @NgModule({ imports: [StoreModule.forRoot(reducers)] }) export class StateModule {} IMPORTING APP REDUCER IN MODULE IMPORT STORE MODULE AND PASS REDUCERS import { NgNodule } from '@angular/core'; import { StateModule } from ‘./state.module’; @NgModule({ imports: [StateModule] }) export class AppModule {} IMPORT STATE MODULE IN APP MODULE
  • 7.
    NG-RX import { FeatureAState} from ‘./redux/featureA.reducer’; constructor( private store: Store<FeatureAState> ){ this.projects$ = store.pipe( select(‘featureA’), map((featureAState: FeatureAState) => featureAState.projects) ) } 7 ACCESSING STORE IN COMPONENT IMPORT FEATURE STATE INTERFACE INJECTING STORE AS SERVICE SELECTING FIELD FROM FEATURE STATE
  • 8.
    NG-RX import { StoreModule} from ‘@ngrx/store’; import { StoreDevToolsModule } from ‘@ngrx/store-devtools'; import { NgNodule } from '@angular/core'; import { reducers } from ‘.’; @NgModule({ imports: [StoreModule.forRoot(reducers), StoreDevtoolsModule.instrument({ maxAge: 10 })] }) export class StateModule {} 8 REDUX DEVTOOLS IMPORT STORE DEV TOOLS MODULE
  • 9.
    NG-RX 9 STRONGLY TYPEDACTIONS export enum FeatureAActionTypes { ProjectFetched = '[FeatureA] Fetch’, ProjectSelected = '[FeatureA] Selected’, } import { Action } from '@ngrx/store'; export class SelectProject implements Action { readonly type = FeatureAActionTypes.ProjectSelected; constructor(private payload: Project) {} } export class FetchProject implements Action { readonly type = FeatureAActionTypes.ProjectFetched; } export type FeatureAAction = SelectProject | FetchProject CREATING ENUM FOR ALL ACTIONS CREATING TYPE OF ACTION WITHOUT PAYLOAD CREATING TYPE OF ACTION WITH PAYLOAD EXPORTING ALL ACTIONS AS TYPE
  • 10.
    NG-RX 10 MODIFY REDUCERTO USE NEW TYPED ACTIONS import { FeatureAActionTypes } from ‘./featureA.actions’; export function featureAReducer( state = initialFeatureAState, action): FeatureAState{ switch(action.type) { case FeatureAActionTypes.ProjectSelected: return { selectedProjectId: action.payload, projects: state.projects } default: return state; } } ) IMPORT ACTION ENUM MAPPING CASES WITH NEW ENUMS
  • 11.
    NG-RX 11 COLLECTION MANAGEMENT exportinterface FeatureAState extends EntityState<Project> { selectedProjectId: string | null; } export interface FeatureAState { projects: Project[], selectedProjectId: string | null; } *OLD STATE AS FROM PREVIOUS SLIDE CHANGING STATE AS ENTITY STATE export interface EntityState<T> { ids: string[] | number[]; entities: Dictionary<T>; } export const adapter: EntityAdapter<Project> = createEntityAdapter<Project>(); export const initialFeatureAState: FeatureAState = { projects: initialProjects, selectedProjectId: null } *OLD INITIAL STATE export const initialState: ProjectsState = adapter.getInitialState({ selectedProjectId: null }) PULLING INITIAL STATE OUT OF ADAPTER import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';
  • 12.
    NG-RX 12 BENEFITS OFENTITY COLLECTION export function featureAReducer( state = initialFeatureAState, action): featureAState { switch (action.type) { case FeatureAActionTypes.ProjectSelected: return Object.assign({}, state, { selectedProjectId: action.payload }); case FeatureAActionTypes.ProjectFetched: return adapter.addMany(action.payload, state); case FeatureAActionTypes.AddProject: return adapter.addOne(action.payload, state); case FeatureAActionTypes.UpdateProject: return adapter.updateOne(action.payload, state); case FeatureAActionTypes.DeleteProject: return adapter.removeOne(action.payload, state); default: return state; } }
  • 13.
    NG-RX ACCESSING ENTITY COLLECTIONIN COMPONENT constructor( private store: Store<FeatureAState> ){ this.projects$ = store.pipe( select(‘featureA’), map((featureAState: FeatureAState) => featureAState.projects) ) } constructor( private store: Store<FeatureAState> ){ this.projects$ = store.pipe( select(‘featureA'), map(data => data.entities), map(data => Object.keys(data).map(k => data[k])) ) } *OLD WAY ENTITY COLLECTION IN REDUX DEV TOOLS NEW WAY (ITS BIT COMPLICATED BUT ONLY IN THIS SLIDE) 13
  • 14.
    NG-RX SELECTORS TO MAKELIFE EASIER export const getSelectedProjectID = (state: FeatureAState) => state.selectedProjectId; LOW LEVEL SELECTOR const { selectIds, selectEntities, selectAll } = adapter.getSelectors(); export const selectProjectIds = selectIds; export const selectProjectEntities = selectEntities; export const selectAllProjects = selectAll; ADAPTER SELECTORS import { createFeatureSelector, createSelector } from '@ngrx/store' export const selectFeatureAState = createFeatureSelector<FeatureAState>(‘featureA'); export const selectProjectIds = createSelector( selectProjectsState, fromFeatureA.selectProjectIds ); export const selectProjectEntities = createSelector( selectProjectsState, fromFeatureA.selectProjectEntities ); export const selectAllProjects = createSelector( selectProjectsState, fromFeatureA.selectAllProjects ); 14
  • 15.
    NG-RX ACCESSING STATE BYSELECTOR IN COMPONENT constructor( ... this.projects$ = store.pipe( select(selectAllProjects) ); constructor( private store: Store<FeatureAState> ){ this.projects$ = store.pipe( select(‘featureA'), map(data => data.entities), map(data => Object.keys(data).map(k => data[k])) ) } FROM THAT SHIT TO THIS BEAUTY 15
  • 16.
    NG-RX EFFECTS TO HANDLESIDE EFFECTS (ASYNC) export enum FeatureAActionTypes { ProjectFetch = '[FeatureA] Fetch’, ProjectFetched = '[FeatureA] Fetch Success’, ProjectSelected = '[FeatureA] Selected’, } SPLIT ACTIONS INTO STAGES @Injectable({providedIn: 'root'}) export class FeatureAEffects { constructor( private actions$: Actions, private projectsService: ProjectsService ) { } @Effect() fetchProjects$ = this.actions$.pipe( ofType(FeatureAActionTypes.ProjectFetch), switchMap((action: FetchProject)) => this.projectsService.all() .pipe(map((res: Project[]) => new ProjectFetched)) ) ); TRIGGER EVENT COMPLETION EVENT @NgModule({ imports: [ ... EffectsModule.forRoot([ CustomersEffects, ProjectsEffects ]) ] }) FINALLY IMPORT IT IN MODULE 16
  • 17.
    NG-RX IMPROVE DATA PERSISTENCEIN EFFECTS WITH NX 17 import { DataPersistence } from '@nrwl/nx'; constructor( private actions$: Actions, private dataPersistence: DataPersistence<FeatureAState>, private projectsService: ProjectsService ) { } } @Effect() fetchProjects$ = this.dataPersistence.fetch(FeatureAActionTypes.ProjectFetch, { run: (action: FetchProject, state: FeatureAState) => { return this.projectsService.all().pipe(map((res: Project[]) => new ProjectFetched(res))) }, onError: (action: LoadProjects, error) => { console.log('ERROR', error); } }); @Effect() addProjects$ = this.dataPersistence.pessimisticUpdate(ProjectsActionTypes.addProjects, { run: () => {}, onError: () => {} }); @Effect() addProjects$ = this.dataPersistence.optimisticUpdate(ProjectsActionTypes.addProjects, { run: () => {}, onError: () => {} }); CLEANING UP EFFECTS WITH DATA PERSISTENCE PESSIMISTIC UPDATE OPTIMISTIC UPDATE https://nrwl.io/nx/guide-data-persistence
  • 18.
    NG-RX HANDLING RELATIONAL MODELS 18 exportconst selectFeatureAState = createFeatureSelector<FeatureAState>(‘featureA'); export const selectCurrentProjectId = createSelector( selectFeatureAState, fromFeatureA.getSelectedProjectId ); export const getSelectedProjectID = (state: FeatureAState) => state.selectedProjectId; export const selectCurrentProject = createSelector( selectProjectEntities, selectCurrentProjectId, (projectEntities, projectId) => return projectId ? projectEntities[projectId] : null ; ) const { selectIds, selectEntities, selectAll } = adapter.getSelectors(); export const selectProjectEntities = selectEntities; export const selectCustomrsProjects = createSelector( selectAllCustomers, selectAllProjects, (customers, projects) => {} ) COMPOSING SELECTORS FROM SAME MODEL COMPOSING SELECTORS FROM DIFFERENT MODEL
  • 19.