Introducing @ngrx/entity

One of the most consistent complaints about building apps with NgRx is the amount of boilerplate developers have to write. The challenge when trying to solve the boilerplate issue is that the required explicitness is essential to get the full benefits out of the architecture. We think that by developing targeted libraries we can help reduce some of this boilerplate while retaining all of the benefits that NgRx provides.

Today we are excited to announce the release of @ngrx/entity, the first such library from the NgRx team designed to reduce boilerplate. Its goal is to help developers write reducer functions that maintain collections of entities.

How it Works

@ngrx/entity lets you create entity adapters for different kinds of entities. Using an entity adapter, you can quickly write reducer operations and automatically generate selectors. Pulling from our example application, let’s say we want to write an application that manages a collection of books. The interface for books looks like this:

interface Book {
id: string;
title: string;
}

The first step to writing a reducer that manages this collection is to create an entity adapter:

import { createEntityAdapter } from '@ngrx/entity';
const bookAdapter = createEntityAdapter<Book>();

Next we need to declare the interface for our books state:

import { EntityState } from '@ngrx/entity';
export interface BookState extends EntityState<Book> { }

The shape of EntityState looks like this:

interface EntityState<V> {
ids: string[];
entities: { [id: string]: V };
}

There are two primary reasons we maintain a list of ids and a dictionary of entities:

  1. We want to make looking up a specific entity really fast. If you wanted to just select one book from the store, using the entities dictionary is much faster than searching through an array
  2. We also want to maintain the order of the list. This is especially important if you want to keep the list sorted!

The shape of EntityState<V> meets both goals. It’s also extendable, so we can include other pertinent information in the collection of Books, such as the currently selected book.

Next we define some actions:

import { Action } from '@ngrx/store';
export enum BookActionTypes {
ADD_ONE = '[Books] Add One',
UPDATE_ONE = '[Books] Update One',
DELETE_ONE = '[Books] Delete One',
GET_ALL = '[Books] Get All'
}
export class AddOne implements Action {
readonly type = BookActionTypes.ADD_ONE;
  constructor(public book: BookModel) { }
}
export class UpdateOne implements Action {
readonly type = BookActionTypes.UPDATE_ONE;
  constructor(
public id: string,
public changes: Partial<BookModel>,
) { }
}
export class DeleteOne implements Action {
readonly type = BookActionTypes.DELETE_ONE;
  constructor(public id: string) { }
}
export class GetAll implements Action {
readonly type = BookActionTypes.GET_ALL;
  constructor(public books: BookModel[]) { }
}
export type BookActions
= GetOne
| UpdateOne
| DeleteOne
| GetAll;

Now we are ready to use the bookAdapter to create our book reducer:

const initialState: BookState = bookAdapter.getInitialState();
export function bookReducer(
state: BookState = initialState,
action: BookActions,
): BookState {
switch (action.type) {
case BookActionTypes.ADD_ONE:
return bookAdapter.addOne(action.book, state);
    case BookActionTypes.UPDATE_ONE:
return bookAdapter.updateOne({
id: action.id,
changes: action.changes,
}, state);
    case BookActionTypes.DELETE_ONE:
return bookAdapter.deleteOne(action.id, state);
    case BookActionTypes.GET_ALL:
return bookAdapter.addAll(action.books, state);
    default:
return state;
}
}

The new piece of state can be registered in the Store using the newly created reducer. The last thing we need to do is generate selectors for working with this state:

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

So what boilerplate did we save?

  1. No longer have to explicitly declare all of the properties for the state interface
  2. The implementation for adding, deleting, or updating entities in the state are all handled by the adapter.
  3. The adapter generates a set of commonly used selectors for you.

More documentation about Entity is available in the NgRx GitHub repo and usage in the example application.

NgRx 4.1 Released

In addition to releasing @ngrx/entity, we have also published v4.1.0 of Store, Effects, and Router Store that includes a number of new features and bug fixes. For more information on what is included please check the changelog.

Help Support NgRx

With NgRx 4 we announced the creation of our OpenCollective where you can help support the development of NgRx. We are very thankful for the support received from our backers, sponsors and community. Entity is the first the library created as a result of those contributions. Entity is the first step in creating more libraries for the NgRx platform. Consider supporting these development efforts through the OpenCollective.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.