Why nobody told

me about ngrx/entity ?

Gerard Sans |  @gerardsans

Google Developer Expert

Google Developer Expert

International Speaker

Spoken at 65 events in 23 countries

Blogger

Blogger

Trainer

Trainer

Redux

State Management

ngrx

Redux

Dan Abramov

@gaearon

Redux Principles

  • Unidirectional data flow
  • Single Store
  • No side-effects

Unidirectional data flow

source: blog

1

2

3

4

5

Developer

Experience

extension.remotedev.io

Features

  • Save/Restore State
  • Live Debugging
  • Time travel
  • Dispatch Actions

Todo App

demo

ngrx

v5

What's new in v5?

  • @ngrx/schematics (@angular-devkit)
  • 'Pipeable' select operator (lettable)
  • Support custom createSelector
  • RxJS 5.5
  • UpsertOne/Many

Rob Wormald

@robwormald

  • Suite including
    • @ngrx/store*
    • @ngrx/effects
    • @ngrx/router-store
    • @ngrx/store-devtools*
    • @ngrx/entity*
    • @ngrx/schematics*
  • Re-implementation of Redux on top Angular and RxJS 5.
  • Colocation Support
    • StoreModule.forRoot()
    • StoreModule.forFeature()
  • Payload Type checking
  • Asynchronous Actions Middleware
  • Executes side-effects
  • Colocation Support
    • EffectsModule.forRoot()
    • EffectsModule.forFeature()

Asynchronous Actions

source: blog

1

2

3

4

5

A

A

ngrx/entity

  • Library to manage Lists
  • High performance
    • EntityState<T>
    • Ids lookup and Entities Map
  • Reducer Helper (EntityAdapter)
    • getInitialState
    • Add/update/remove: All, One, Many
    • getSelectors: Ids, Entities, All, Total
  • Scaffolding templates for ngrx
  • Provides commands for
    • Setting initial Store and Effects
    • Actions, Reducers, Entities, Features
    • Containers + Store injected
  • Automates creation + registration

ngrx/schematics commands

> ng new ngrx-entity-todo

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

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


> ng generate entity Todo -m app.module.ts --collection @ngrx/schematics

Filter Actions

// src/app/reducers/currentFilter/currentFilter.actions.ts
import { Action } from '@ngrx/store';

export enum CurrentFilterActionTypes {
  SetCurrentFilter = '[Filter] Set current filter'
}

export class SetCurrentFilter implements Action {
  readonly type = CurrentFilterActionTypes.SetCurrentFilter;

  constructor(public payload: { filter: string }) {}
}

export type CurrentFilterActions = SetCurrentFilter;

Filter Reducer

// src/app/reducers/currentFilter/currentFilter.reducer.ts
export const initialState: string = 'SHOW_ALL';

export function reducer(
  state = initialState, 
  action: CurrentFilterActions): string 
{
  switch (action.type) {
    case CurrentFilterActionTypes.SetCurrentFilter:
      return action.payload.filter
    default:
      return state;
  }
}

Todos Actions

// src/app/reducers/todo/todo.reducer.ts
export enum TodoActionTypes {
  AddTodo = '[Todo] Add Todo', UpdateTodo = '[Todo] Update Todo', ...
}

let currentId = 1;
export class AddTodo implements Action {
  readonly type = TodoActionTypes.AddTodo;
  constructor(public payload: { todo: Todo }) {
    payload.todo.id = currentId++;
  }
}

export type TodoActions = LoadTodos | AddTodo | UpsertTodo | AddTodos 
| UpsertTodos | UpdateTodo | UpdateTodos | DeleteTodo | DeleteTodos | ClearTodos;

Todos Model

// src/app/reducers/todo/todo.model.ts
import { EntityState } from '@ngrx/entity';

export interface Todo {
  id: number;
  completed: boolean;
  text: string;  
}

export interface Todos {
  todos: EntityState<Todo>;
}

Composing Reducers

import * as todos from './todo/todo.reducer';
import * as currentFilter from './currentFilter/currentFilter.reducer';

export interface Filter { 
  currentFilter: string;
}
export interface Todos {
  todos: EntityState<Todo>;
}
export interface TodosState extends Todos, Filter { }

export const reducers: ActionReducerMap<TodosState> = {
  todos: todos.reducer,
  currentFilter: currentFilter.reducer
};

New Pipeable Selectors

// ngrx v4
// export const getTodos = state$ => state$.select(s => s.todos); 
// export const getCurrentFilter = state$ => state$.select('currentFilter'); 

export const getTodos = state$ => state$.pipe(
  select('todos'), 
  map(todoEntity.selectAll) 
);

export const getCurrentFilter = state$ => state$.pipe(
  select('currentFilter')
);

AppModule Setup

import { StoreModule } from "@ngrx/store";
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { AppComponent } from './app.component';
import { reducers, metaReducers } from './reducers';

@NgModule({
  imports: [ 
    BrowserModule, 
    StoreModule.forRoot(reducers, { metaReducers }),
    !environment.production ? StoreDevtoolsModule.instrument() : [],
  ],
  declarations: [ AppComponent ],
  bootstrap: [ AppComponent ]
})
export class AppModule {}

Let's use it!

Adding a new Todo

  1. Component subscribes
  2. Component dispatches TodoActionTypes.AddTodo
  3. Store executes rootReducer
  4. Store notifies Component
  5. View updates

1

2

3

4

5

Subscribing to the Store

@Component({
template: `
  <todo *ngFor="let todo of todos | async | visibleTodos:currentFilter">
    {{todo.text}}
  </todo>`
})
export class App implements OnDestroy {
  constructor(private _store: Store<TodosState>) { }

  public ngOnInit() {
    this.todos = this._store.let(getTodos);
    this._store.let(getCurrentFilter).subscribe((filter: string) => {
      this.currentFilter = filter;
    })
  }
}

app.component.ts

@Component({
  template: `<input #todo (keydown.enter)="addTodo(todo1)">`
})
export class AppComponent {
  private addTodo(input) {
    if (input.value.length === 0) return
    const todo: Todo = {
      id: null,
      text: input.value,
      completed: false,
    }
    this._store.dispatch(new todoActions.AddTodo({ todo }))
    input.value = ''
  }
}

TodoActionTypes.AddTodo

// add new todo
{
  type: '[Todo] Add Todo',
  payload: {
    todo: {
      id: 1,
      text: 'Learn Dutch',
      completed: false
    }
  }
}

todos Reducer

export const adapter: EntityAdapter<Todo> = createEntityAdapter<Todo>();
export let initialState: State = adapter.getInitialState();

export function reducer(state = initialState, action: TodoActions): State {
  switch (action.type) {
    case TodoActionTypes.AddTodo: {
      return adapter.addOne(action.payload.todo, state);
    }
    ...
    default: { return state; }
  }
}

export const { 
  selectIds, selectEntities, selectAll, selectTotal 
} = adapter.getSelectors();

State

{
  todos: {
    ids: [ 1 ],
    entities: {
      '1': {
        id: 1,
        text: 'Learn Dutch',
        completed: false
      }
    }
  },
  currentFilter: 'SHOW_ALL'
}

More

@MikeRyanDev

@robwormald

@brandontroberts

Rob Wormald

Mike Ryan

Brandon Roberts

@toddmotto

Todd Motto

gsans/ngrx-entity-todo-app

Why nobody told me about ngrx/entity?

By Gerard Sans

Why nobody told me about ngrx/entity?

In this talk we are going to explore one of the latest additions to the ngrx suite. This is the ngrx/entity! We will use an existing ngrx Application to identify repetitive code and show how we can use ngrx/entity to keep our reducers clean and shiny!

  • 3,663