Angular State Management using ngRx

Why use ngRx?

Service

Component

Service

Component

Service

Component

Service

Component

Service

Component

Service

Component

Service

Component

Component

Component

Service

Increasing complexity

I have no idea

what I'm doing

Redux is a library, and it is also a pattern

Unidirectional Data Flow

View

Action

Store

current state

iteraction

dispatch action

reducer

state / action

new state

@ngrx/platform (v5)

  • @ngrx/store
  • @ngrx/effects
  • @ngrx/entity
  • @ngrx/store-devtools
  • @ngrx/router-store
  • @ngrx/schematics (new in v5)

@ngrx/store

Installation

> ng new angular-ngrx-example

> npm install @ngrx/{store, effects, entity, store-devtools} --save

Setup

import { StoreRouterConnectingModule } from '@ngrx/router-store';
import { StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';

// ...

@NgModule({
  imports: [
    StoreModule.forRoot(reducers),
    EffectsModule.forRoot([]),
    StoreRouterConnectingModule,
    !environment.production ? StoreDevtoolsModule.instrument({ maxAge: 50 }) : []
  ]
})
// ...

Setup with lazy loading

@NgModule({
  imports: [
    StoreModule.forFeature('task', taskReducer),
    EffectsModule.forFeature([TaskEffects])
  ]
})
export class TasksModule {}

State

A plain JavaScript object

{
    tasks: [
      {
        id: '1',
        title: 'Task 01',
        completed: false
      },
      {
        id: '2',
        title: 'Task 02',
        completed: true
      }
    ],
    isLoading: false,
    error: null
}

Define the state

// step 1: define state and initial state
export interface TaskState {
  tasks: Task[];
  isLoading: boolean;
  error: any;
}

export const taskInitialState: TaskState = {
  tasks: [],
  isLoading: true,
  error: null
};


// selectors - select an information from the state
export const taskState = createFeatureSelector<TaskState>('task');
export const selectedRecords = createSelector(taskState, (state: TaskState) => state.tasks);
export const selectIsLoading = createSelector(taskState, (state: TaskState) => state.isLoading);

Action

Request for changing the sate

What's the component responsability?

export enum TaskActionTypes {
  LOAD   = '[Task] LOAD Requested',
  CREATE = '[Task] CREATE Requested',
  UPDATE = '[Task] UPDATE Requested',
  REMOVE = '[Task] REMOVE Requested',
  ERROR  = '[Task] Error'
}

Payload

What information needs to be included in each Action?

export class CreateAction implements Action {

  type = TaskActionTypes.CREATE;

  constructor(public payload: { task: Task }) { }
}

Reducer

A pure function that accepts a state and an action and returns a new state

export const taskReducer: ActionReducer<Task[]> = 
          (state: Task[] = [], action: TaskAction) => {
  

switch (action.type) {

    case TaskActionTypes.CREATE:
      return [...state, action.payload.task];


    case TaskActionTypes.REMOVE:
      return state.filter((task: Task) => {
        return task.id !== action.payload.task.id;
      });

    default:
      return state;
  }
};

Dispatch an action

export class TasksComponent implements OnInit {
  
  constructor(private store: Store<AppState>) {}

  ngOnInit() {

    this.store.dispatch(new Action.LoadAction());
   
  }
);

Select a state

export class TasksComponent implements OnInit {

  tasks$: Observable<Task[]>;
  
  constructor(private store: Store<AppState>) {}

  ngOnInit() {

    this.store.dispatch(new Action.LoadAction());

    this.tasks$ = this.store.select(State.selectedRecords)

  }
);
export const taskState = createFeatureSelector<TaskState>('task');
export const selectedRecords = createSelector(taskState, (state: TaskState) => state.tasks);

async pipe for the win!

<mat-card>
    <app-task-form (createTask)="onCreateTask($event)"></app-task-form>

    <mat-spinner *ngIf="isLoading$ | async; else taskList"></mat-spinner>

    <ng-template #taskList>
      <app-tasks-list
        [tasks]="tasks$"
        (remove)="onRemoveTask($event)"
        (edit)="onUpdateTask($event)">
      </app-tasks-list>
    </ng-template>

  <div class="error-msg" *ngIf="error$ | async as error">
    <p>{{ error }}</p>
  </div>

</mat-card>

Presentational Components

@Component({
  selector: 'app-task-item',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TaskItemComponent {

  @Input() task: Task;
  @Output() remove: EventEmitter<any> = new EventEmitter(false);
  @Output() edit: EventEmitter<any> = new EventEmitter(false);

  onRemove() {
    this.remove.emit();
  }

  onEdit() {
    this.edit.emit(this.task);
  }
}

@ngrx/effects

Async Actions

Request for changing the sate

What's the component responsability?

export enum TaskActionTypes {
  LOAD = '[Task] LOAD Requested',
  LOAD_SUCCESS = '[Task] LOAD Success',
  CREATE = '[Task] CREATE Requested',
  CREATE_SUCCESS = '[Task] CREATE Success',
  UPDATE = '[Task] UPDATE Requested',
  UPDATE_SUCCESS = '[Task] UPDATE Success',
  REMOVE = '[Task] REMOVE Requested',
  REMOVE_SUCCESS = '[Task] REMOVE Success',
  ERROR = '[Task] Error'
}

Effects

Make the request and wait for the response to complete the action

@Effect()
  createAction$ = this.actions$.pipe(

    ofType<Action.CreateAction>(Action.TaskActionTypes.CREATE),
    map(action => action.payload),
    mergeMap(payload =>

      this.api.create(payload.task).pipe(
        map(res => new Action.CreateActionSuccess({ task: res })),
        catchError(error => this.handleError(error)))
    ));

@ngrx/entity

@ngrx/entity

  • Since ngrx v4
  • Manage Collections (Dictionary)
    • Lookup ids
  • Help with CRUDs
    • Create, update, remove: All, One, Many
  • Selectors:
    • ids, all, array, total

Interfaces

export interface EntityState<T> {
    ids: string[];
    entities: Dictionary<T>;
}

export interface EntityStateAdapter<T> {
    addOne<S extends EntityState<T>>(entity: T, state: S): S;
    addMany<S extends EntityState<T>>(entities: T[], state: S): S;
    addAll<S extends EntityState<T>>(entities: T[], state: S): S;
    removeOne<S extends EntityState<T>>(key: string, state: S): S;
    removeMany<S extends EntityState<T>>(keys: string[], state: S): S;
    removeAll<S extends EntityState<T>>(state: S): S;
    updateOne<S extends EntityState<T>>(update: Update<T>, state: S): S;
    updateMany<S extends EntityState<T>>(updates: Update<T>[], state: S): S;
}

export declare type EntitySelectors<T, V> = {
    selectIds: (state: V) => string[];
    selectEntities: (state: V) => Dictionary<T>;
    selectAll: (state: V) => T[];
    selectTotal: (state: V) => number;
};

Reducer with entities

    case TaskActions.LOAD_SUCCESS: {
      return adapter.addMany(action.payload.tasks, state);
    }

    case TaskActions.CREATE_SUCCESS: {
      return adapter.addOne(action.payload.task, state);
    }

    case TaskActions.UPDATE_SUCCESS: {
      return adapter.updateOne(action.payload.task, state);
    }

    case TaskActions.UPSERT_SUCCESS: {
      return adapter.upsertMany(action.payload.tasks, state);;
    }

@ngrx/store-devtools

@ngrx/schematics

 

Installation

> npm install @ngrx/schematics --save-dev

> ng generate store State --root --module app.module.ts --collection @ngrx/schematics

> ng generate entity tasks/store/Task --module tasks/tasks.module.ts --collection @ngrx/schematics

> ng generate effect tasks/store/Task --module tasks/tasks.module.ts --collection @ngrx/schematics

https://github.com/johnpapa/angular-ngrx-data

Main setup

Feature module +

store feature module

Takeaways

  • Great when project becomes complex
  • Makes easier to scale the project
  • Easier to write tests
  • Angular change detection
  • Can be used wherever necessary
  • Workarounds available for boilerplate code

Thank You!

To learn more...

Made with Slides.com