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...

NgRx: Angular State Management

By Loiane Groner

NgRx: Angular State Management

In this talk, you will learn how to manage state in an Angular application using the ngrx suite (Redux for Angular powered by RxJS) including store, effects, entities, schematics. You will learn when to use ngrx and the pros and cons of adopting this tool in your project.

  • 2,176