Using @ngrx/entity in Angular to Simplify Store Reducers

Redux and @ngrx/store reducers for CRUD operations often have a similar shape and end up being mostly boilerplate code. To help simplify and cut on the boilerplate, the ngrx organization created a new library called @ngrx/entity that can used with @ngrx/store in Angular 2+ projects. With @ngrx/entity, resources are known as entities (e.g.: users, todos, tickets, …) and we can create entity adapters that have built-in methods for CRUD reducer actions (create, read, update, delete).

We’ll demonstrate how to use @ngrx/entity in this post via a simple todo app example, very similar to the one we implemented in our intro to @ngrx/store.

Initial Actions, Reducer and Model

First, let’s see how we would implement the todo app without @ngrx/entity. Here’s our todo model interface:

models/todo.model.ts

export interface Todo {
  id: string;
  done: boolean;
  value: string;
}

And our 4 possible actions for todos are defined like the following with distinct classes that implement @ngrx/store’s Action interface:

actions/todo.actions.ts

import { Action } from '@ngrx/store';
import { Todo } from '../models/todo.model';

export const ADD_TODO = 'ADD_TODO';
export const DELETE_TODO = 'DELETE_TODO';
export const UPDATE_TODO = 'UPDATE_TODO';
export const TOGGLE_DONE = 'TOGGLE_DONE';

export class AddTodo implements Action {
  readonly type = ADD_TODO;

  constructor(public payload: { todo: Todo }) {}
}

export class DeleteTodo implements Action {
  readonly type = DELETE_TODO;

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

export class UpdateTodo implements Action {
  readonly type = UPDATE_TODO;

  constructor(public payload: { id: string; newValue: string }) {}
}

export class ToggleDone implements Action {
  readonly type = TOGGLE_DONE;

  constructor(public payload: { id: string; done: boolean }) {}
}

export type TodoActions = AddTodo | DeleteTodo | UpdateTodo | ToggleDone;

These actions will stay exactly the same when using @ngrx/entity.

Our todo reducer looks like this:

todo.reducer.ts

import * as todoActions from '../actions/todo.actions';

export function todoReducer(state = [], action: todoActions.TodoActions) {
  switch (action.type) {
    case todoActions.ADD_TODO:
      return [action.payload.todo, ...state];
    case todoActions.DELETE_TODO:
      return state.filter(item => item.id !== action.payload.id);
    case todoActions.UPDATE_TODO:
      return state.map(item => {
        return item.id === action.payload.id
          ? Object.assign({}, item, { value: action.payload.newValue })
          : item;
      });
    case todoActions.TOGGLE_DONE:
      return state.map(item => {
        return item.id === action.payload.id
          ? Object.assign({}, item, { done: action.payload.done })
          : item;
      });
    default:
      return state;
  }
}

Notice how our reducer contains all the boilerplate for adding, updating, toggling and deleting todo items. This is where @ngrx/entity will be especially helpful.

Hooking Things Up In the App Component

Here’s how our app component looks like, selecting the todos from the store and dispatching actions:

app.components.ts

import { Component, OnInit } from '@angular/core';

import { UUID } from 'angular2-uuid';
import { Store } from '@ngrx/store';
import * as TodoActions from './actions/todo.actions';

import { Todo } from './models/todo.model';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styles: [`.done { text-decoration: line-through; color: salmon; }`]
})
export class AppComponent implements OnInit {
  todos$: any;
  todo: string;
  editing = false;
  idToEdit: string | null;

  constructor(private store: Store<any>) {}

  generateUUID() {
    return UUID.UUID();
  }

  ngOnInit() {
    this.todos$ = this.store.select('todoReducer');
  }

  addTodo(value) {
    const todo: Todo = {
      value,
      done: false,
      id: this.generateUUID()
    };
    this.store.dispatch(new TodoActions.AddTodo({ todo }));

    this.todo = '';
  }

  deleteTodo(id) {
    this.store.dispatch(new TodoActions.DeleteTodo({ id }));
  }

  editTodo(todo) {
    this.editing = true;
    this.todo = todo.value;
    this.idToEdit = todo.id;
  }

  cancelEdit() {
    this.editing = false;
    this.todo = '';
    this.idToEdit = null;
  }

  updateTodo(updatedTodo) {
    this.store.dispatch(
      new TodoActions.UpdateTodo({
        id: this.idToEdit,
        newValue: updatedTodo
      })
    );
    this.todo = '';
    this.idToEdit = null;
    this.editing = false;
  }

  toggleDone(todo) {
    this.store.dispatch(
      new TodoActions.ToggleDone({ id: todo.id, done: !todo.done })
    );
  }
}

We make use of a small utility called angular2-uuid to create a unique ID for our todos. You can install it in our project like this:

$ yarn add angular2-uuid

# or, via npm:
$ npm install angular2-uuid

And the final piece is the component’s template, which can look a little bit like this:

app.component.html

<input placeholder="your todo" [(ngModel)]="todo">

<button (click)="addTodo(todo)" [disabled]="!todo" *ngIf="!editing">
  Add todo
</button>

<button (click)="updateTodo(todo)" *ngIf="editing">
  Update
</button>
<button (click)="cancelEdit()" *ngIf="editing">
  Cancel
</button>


<ul>
  <li *ngFor="let todo of todos$ | async">
    <span [class.done]="todo.done"></span>
    <button (click)="editTodo(todo)">Edit</button>
    <button (click)="toggleDone(todo)">Toggle Done</button>
    <button (click)="deleteTodo(todo.id)">X</button>
  </li>
</ul>

🎉 Now our todo app is working and properly using @ngrx/store for state management. Next we’ll setup @ngrx/entity and demonstrate how the reducer can be vastly simplified.

Enter @ngrx/entity: Installation & Setup

Install @ngrx/entity through npm or Yarn:

$ yarn add @ngrx/entity
# or, using npm
$ npm install @ngrx/entity

Now we’ll create a feature module for our todos. In this simple app having a separate module is overkill, but most apps that will benefit from something like @ngrx/entity will have multiple features, and therefore benefit from separating features into their own modules.

Here’s what our todo module looks like:

todo.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { StoreModule } from '@ngrx/store';
import { todoReducer } from './reducers/todo.reducer';

@NgModule({
  imports: [
    CommonModule,
    StoreModule.forFeature('todo', todoReducer)
  ],
  declarations: []
})
export class TodoModule {}

We import StoreModule and call forFeature on it, giving it a name for the feature and a reducer.


Our app module changes a little bit to include a reducer map called reducers that we’ll define in a little bit that’s passed-in to StoreModule’s forRoot method:

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';

import { StoreModule } from '@ngrx/store';
import { reducers } from './reducers';
import { TodoModule } from './todo.module';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    FormsModule,
    StoreModule.forRoot(reducers),
    TodoModule
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Now for our todo reducer, we change it to something like the following. Pay attention to the highlighted parts:

./reducers/todo.reducer.ts

import { createEntityAdapter, EntityState, EntityAdapter } from '@ngrx/entity';

import { Todo } from '../models/todo.model';

import * as todoActions from '../actions/todo.actions';

export interface State extends EntityState<Todo> {}

export const todoAdapter: EntityAdapter<Todo> = createEntityAdapter<Todo>();

export const initialState: State = todoAdapter.getInitialState();

export function todoReducer(
  state: State = initialState,
  action: todoActions.TodoActions
) {
  switch (action.type) {
    case todoActions.ADD_TODO:
      return todoAdapter.addOne(action.payload.todo, state);
    case todoActions.TOGGLE_DONE:
      return todoAdapter.updateOne(
        { id: action.payload.id, changes: { done: action.payload.done } },
        state
      );
    case todoActions.UPDATE_TODO:
      return todoAdapter.updateOne(
        { id: action.payload.id, changes: { value: action.payload.newValue } },
        state
      );
    case todoActions.DELETE_TODO:
      return todoAdapter.removeOne(action.payload.id, state);
    default:
      return state;
  }
}

Notice how most of our reducer boilerplate is gone and instead we call methods on an adapter that we create using createEntityAdapter. The available adapter methods are addOne, addMany, addAll, removeOne, removeMany, removeAll, updateOne and updateMany.

Our adapter also has a handy getInitialState method to create an initial state that’s properly typed.


Next we need to create the ActionReducerMap that’s imported in our app module for StoreModule’s forRoot method. On top of that, we’ll create a selectAllTodos selector that selects all our todos from the store:

./reduces/index.ts

import {
  createSelector,
  createFeatureSelector,
  ActionReducerMap
} from '@ngrx/store';
import * as fromTodo from './todo.reducer';

export const reducers: ActionReducerMap<any> = {
  todo: fromTodo.todoReducer
};

export const selectTodoState = createFeatureSelector<fromTodo.State>('todo');

export const { selectAll: selectAllTodos } = fromTodo.todoAdapter.getSelectors(
  selectTodoState
);

Our todoAdapter adapter instance has a getSelectors method that takes-in the feature to select from the state and then returns selectAll, selectEntities, selectIds and selectTotal. In this case, we only need selectAll and we rename it to selectAllTodos.


Back in our component almost everything can stay exactly the same, and the only thing that changes is how we select from the store:

app.component.ts (partial)

import { selectAllTodos } from './reducers';

// ...

export class AppComponent implements OnInit {
  // ...

  ngOnInit() {
    this.todos$ = this.store.select(selectAllTodos);
  }

  // ...
}

😎 And there you have it, a way to simplify your reducers and get on with building great apps!

  Tweet It
✖ Clear

🕵 Search Results

🔎 Searching...