Search Lessons, Code Snippets, and Videos
search by algolia
X
#native_cta# #native_desc# Sponsored by #native_company#

Ngrx Entity CRUD Feature Module Tutorial

Episode 60 written by Jeff Delaney
full courses and content on fireship.io


Ngrx entity streamlines process of creating a collection of objects. It makes a few basic assumptions about the shape of your data, then provides a handful of tools that

In this lesson, I am going to build a basic CRUD (create, update, delete) app for customizing a pizza order. In addition, I will show you how to build a simple ngrx feature module for better organization of your code.

Get the full source code.

If you’re brand new to the world of Redux, make sure to check out my ngrx Quick Start before attempting this tutorial. I will not be spending very much time explaining the basic underlying concepts in this lesson.

What is an Entity?

working example of ngrx entity in Angular

Meditate on this interface for a while.

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

The EntityState interface describes the shape of your data when using entity. In this demo, we are going to create a pizza entity that will hopefully make sense out of this concept.

I find that an entity becomes clearer when I look at it as a POJO.

entities = { 
ids: ['pizza1', 'pizza2', 'pizza3'],
entities: {
'pizza1': {
toppings: 'anchovies',
size: 'large'
},
'pizza2': {
...data
},
'pizza3': {
...data
}
}
}

Why the array of ids? JavaScript objects do not maintain order, but arrays do. Keeping an array of ids makes it possible to maintain order.

This structure makes it possible for ngrx to provide a bunch or helper (adapter) methods that make reducers more expressive and consistent.

How to Create an Ngrx Feature Module

It is a good idea to use feature modules in Ngrx to keep your reducers, actions, and models maintainable.

Pizza Module

ng g module pizza
ng g component pizza/pizza-order -m pizza

First, run the commands above to create a module and component, then manually create the pizza/pizza.reducer.ts and pizza/pizza.actions.ts inside the feature directory.

Your final pizza module should looks something like this:

import { NgModule } from '@angular/core';
import { PizzaOrderComponent } from './pizza-order/pizza-order.component'

import { CommonModule } from '@angular/common';
import { StoreModule } from '@ngrx/store';
import { pizzaReducer } from './pizza.reducer';

@NgModule({
imports: [
CommonModule,
StoreModule.forFeature('pizza', pizzaReducer)
],
exports: [PizzaOrderComponent],
declarations: [PizzaOrderComponent]
})
export class PizzaModule { }

Reducer Index

When working with feature modules, we need a centralized place to bundle up the reducers. Create the following file app/reducers/index.ts. The purpose of this file is to map multiple reducers together so that they can used in the store in the next step.

import { ActionReducerMap } from '@ngrx/store';
import { pizzaReducer } from '../pizza/pizza.reducer';

export const reducers: ActionReducerMap<any> = {
pizza: pizzaReducer
};

App Module

Now we can bring everything together in the app module. Notice I am also adding Redux Dev Tools to help us out with debugging.

// omitted other imports
import { StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';

import { PizzaModule } from './pizza/pizza.module';
import { reducers } from './reducers';


@NgModule({
declarations: [
AppComponent,
],
imports: [
BrowserModule,
AppRoutingModule,
StoreModule.forRoot(reducers),
StoreDevtoolsModule.instrument({
maxAge: 25
}),
PizzaModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

Define the Actions

Actions are basically the same with @ngrx/entity, but you will need to be careful to make sure the payload data is appropriate for the adapter method. My goal is to keep this super simple, so let’s just make CRUD - Create, Update, and Delete.

import { Action } from '@ngrx/store';
import { Pizza } from './pizza.reducer';


export const CREATE = '[Pizzas] Create'
export const UPDATE = '[Pizzas] Update'
export const DELETE = '[Pizzas] Delete'

export class Create implements Action {
readonly type = CREATE;
constructor(public pizza: Pizza) { }
}

export class Update implements Action {
readonly type = UPDATE;
constructor(
public id: string,
public changes: Partial<Pizza>,
) { }
}

export class Delete implements Action {
readonly type = DELETE;
constructor(public id: string) { }
}

export type PizzaActions
= Create
| Update
| Delete;

Using @ngrx/entity

I am adding all of the entity code in the reducer to avoid a confusing mix of imports in this tutorial, but you might want to refactor the interface to a dedicated model file.

The first thing we need is an interface that describes our pizza data.

import * as actions from './pizza.actions';
import { EntityState, createEntityAdapter } from '@ngrx/entity';
import { createFeatureSelector } from '@ngrx/store';

// Main data interface

export interface Pizza {
id: string;
size: string;
}

// Entity adapter
export const pizzaAdapter = createEntityAdapter<Pizza>();
export interface State extends EntityState<Pizza> { }


// Default data / initial state

const defaultPizza = {
ids: ['123'],
entities: {
'123': {
id: '123',
size: 'small'
}
}
}

export const initialState: State = pizzaAdapter.getInitialState(defaultPizza);

Reducer Function with the Entity Adapter Methods

The adapter has a number of built-in methods the for handling crud operations. Rather than use Object.asssign({}, state, data) or the spread syntax { state, ...data }, you pass arguments to one of the methods on the entity adapter.

Here’s the full list of the entity adapter methods, the names are self-describing:

// Reducer

export function pizzaReducer(
state: State = initialState,
action: actions.PizzaActions) {

switch (action.type) {

case actions.CREATE:
return pizzaAdapter.addOne(action.pizza, state);

case actions.UPDATE:
return pizzaAdapter.updateOne({
id: action.id,
changes: action.changes,
}, state);

case actions.DELETE:
return pizzaAdapter.removeOne(action.id, state)

default:
return state;
}

}

Selectors

The final step is to create some selectors that can be used to access parts of the store. There are several methods built into @ngrx/entity, but you can also create your own. They allow you to slice subsets of the entity data as an Observable that can be displayed to the end user.

// Create the default selectors

export const getPizzaState = createFeatureSelector<State>('pizza');

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

And Finally… The Component

Now that we have all the ngrx resources wired up, it’s time to create a UI for the end-user.

import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import * as actions from '../pizza.actions';
import * as fromPizza from '../pizza.reducer';


@Component({
selector: 'pizza-order',
templateUrl: './pizza-order.component.html',
styleUrls: ['./pizza-order.component.sass']
})
export class PizzaOrderComponent implements OnInit {

pizzas: Observable<any>;

constructor(private store: Store<fromPizza.State>) { }

ngOnInit() {
this.pizzas = this.store.select(fromPizza.selectAll)
}

createPizza() {
const pizza: fromPizza.Pizza = {
id: new Date().getUTCMilliseconds().toString(),
size: 'small'
}

this.store.dispatch( new actions.Create(pizza) )
}

updatePizza(id, size) {
this.store.dispatch( new actions.Update(id, { size: size }) )
}

deletePizza(id) {
this.store.dispatch( new actions.Delete(id) )
}

}

The HTML will use the event handlers on button clicks to dispatch the action to the reducer.

<h1>Welcome to Ngrx Pizza!</h1>


<div *ngFor="let pizza of pizzas | async">

<img src="/assets/pizza.jpg" width="200px">

<h3>Pizza ID: {{ pizza.id }} </h3>
<p>Size: {{ pizza.size }}</p>


<button *ngIf="pizza.size==='small'"
(click)="updatePizza(pizza.id, 'large')">
Upgrade to Large
</button>

<button *ngIf="pizza.size==='large'"
(click)="updatePizza(pizza.id, 'small')">
Downgrade to Small
</button>

<button (click)="deletePizza(pizza.id)">DELETE</button>

</div>


<button (click)="createPizza()">CREATE new Pizza</button>

The End

You might have noticed that it takes significantly more code to build apps with ngrx - even with the entity helpers. What you get in return is a codebase that is easier to test and debug. This is the tradeoff that you need to consider when deciding if your project should use a global client-size data store. Please reach out on slack if you want a personalized recommendation.