Angular State Management using ngRx

DevFest Space Coast

Loiane Groner

Java, JavaScript + HTML5, Sencha, Cordova/Ionic, Angular, RxJS + all things reactive

How to manage complex state of Angular applications

Why use ngRx?

Do I need ngRx in my project?

Do I need ngRx to manage every single data in my project?

Service

Component

Service

Component

Service

Component

Service

Component

Service

Component

Service

Component

Service

Component

Component

Component

Service

Increasing complexity

@Component({
  selector: 'app-product-order',
  templateUrl: './product-order.component.html',
  styleUrls: ['./product-order.component.scss']
})
export class ProductOrderComponent implements OnInit {
  constructor(
    private router: Router,
    private route: ActivatedRoute,
    private location: Location,
    private productService: ProductService,
    private clientService: ClientService,
    private addressService: AddressService,
    private userRoleService: UserRoleService,
    private comboboxService: ComboboxService,
    private exportFileService: ExportPDFService
  ) {}
}

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

State

A plain JavaScript object

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

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;
  }
};

@ngrx/platform (v5)

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

@ngrx/store

@ngrx/store

  • Redux for Angular powered by RxJS

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 {}

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);

Define actions, reducers

export enum TaskActions {
  LOAD = '[Task] LOAD Requested',
  CREATE = '[Task] CREATE Requested'
}


export class CreateAction implements Action {
  readonly type = TaskActionTypes.CREATE;
  constructor(public payload: { task: Task }) { }
}

export function taskReducer(
  state = taskInitialState, action: TaskAction): TaskState {
  switch (action.type) {

    case TaskActionTypes.CREATE:
      return Object.assign({}, state, {
        tasks: [...state.tasks, action.payload.task]
      });

    // ...
  }
};

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)))
    ));

Service

Service does not know about ngrx or the state

@Injectable()
export class TaskService {
  private readonly API_TASKS_URL = `http://localhost:3001/tasks`;

  constructor(private http: HttpClient) {}

  load() {
    return this.http.get<Task[]>(this.API_TASKS_URL);
  }

  create(record: Task) {
    return this.http.post<Task>(this.API_TASKS_URL, record);
  }

  update(record: Task) {
    return this.http.put<Task>(`${this.API_TASKS_URL}/${record.id}`, record);
  }

  remove(id: string) {
    return this.http.delete<Task>(`${this.API_TASKS_URL}/${id}`);
  }
}

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

Angular State Management using ngRx

By Loiane Groner

Angular State Management using ngRx

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,039