Skip to main content

Angular NgRx Entity

Introduction

Managing collections of data is a common challenge in Angular applications. When building features that require handling lists of items like users, products, or tasks, developers often need to implement the same CRUD (Create, Read, Update, Delete) operations repeatedly. This is where NgRx Entity comes in.

NgRx Entity is an extension of the NgRx library that provides a set of utility functions to simplify the management of entity collections in your application state. It helps reduce boilerplate code when working with arrays of entities that have unique identifiers.

In this guide, you'll learn how to:

  • Set up NgRx Entity in your Angular application
  • Create entity adapters and define entity states
  • Implement CRUD operations using NgRx Entity
  • Use entity selectors to query state efficiently

Prerequisites

Before diving into NgRx Entity, you should have:

  • Basic understanding of Angular
  • Familiarity with NgRx concepts (Store, Actions, Reducers)
  • A working Angular application with NgRx installed

If you're new to NgRx, consider reading the NgRx fundamentals guide first.

Setting Up NgRx Entity

First, you need to install NgRx Entity in your project:

bash
npm install @ngrx/entity

Understanding the Entity Model

NgRx Entity works with collections of objects that have a unique identifier. For example, a User entity might look like this:

typescript
export interface User {
id: number;
name: string;
email: string;
}

The key component here is the id field, which uniquely identifies each user.

Creating an Entity Adapter

The EntityAdapter is the core concept in NgRx Entity. It provides methods to manipulate and query the entity collection.

Here's how to create an entity adapter for our User entity:

typescript
import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';
import { User } from '../models/user.model';

// Create the entity adapter
export const userAdapter: EntityAdapter<User> = createEntityAdapter<User>({
// Configure how to select the entity's ID
selectId: (user: User) => user.id,

// Optional: configure how entities should be sorted
sortComparer: (a: User, b: User) => a.name.localeCompare(b.name),
});

Defining Entity State

Once we have an adapter, we need to define our state interface:

typescript
// Define the entity state structure
export interface UserState extends EntityState<User> {
// Add any additional state properties
selectedUserId: number | null;
loading: boolean;
error: string | null;
}

// Create the initial state
export const initialUserState: UserState = userAdapter.getInitialState({
selectedUserId: null,
loading: false,
error: null
});

The EntityState<User> interface provides two properties:

  1. entities: An object map of entity IDs to entity objects
  2. ids: An array of entity IDs defining the order

Creating Reducers with Entity Adapter

The adapter provides methods like addOne, addMany, updateOne, etc., to manipulate entities in the state:

typescript
import { createReducer, on } from '@ngrx/store';
import * as UserActions from '../actions/user.actions';
import { userAdapter, initialUserState } from './user.state';

export const userReducer = createReducer(
initialUserState,

// Load users
on(UserActions.loadUsers, (state) => ({
...state,
loading: true,
error: null
})),

// Load users success
on(UserActions.loadUsersSuccess, (state, { users }) =>
userAdapter.setAll(users, {
...state,
loading: false
})
),

// Add a user
on(UserActions.addUserSuccess, (state, { user }) =>
userAdapter.addOne(user, state)
),

// Update a user
on(UserActions.updateUserSuccess, (state, { user }) =>
userAdapter.updateOne({
id: user.id,
changes: user
}, state)
),

// Remove a user
on(UserActions.deleteUserSuccess, (state, { id }) =>
userAdapter.removeOne(id, state)
),

// Select a user
on(UserActions.selectUser, (state, { id }) => ({
...state,
selectedUserId: id
}))
);

Entity Adapter Methods

NgRx Entity adapters provide several methods for manipulating state:

MethodDescription
addOneAdd one entity to the collection
addManyAdd multiple entities to the collection
setAllReplace current collection with provided collection
setOneAdd or replace one entity in the collection
removeOneRemove one entity from the collection
removeManyRemove multiple entities from the collection
updateOneUpdate one entity in the collection
updateManyUpdate multiple entities in the collection
upsertOneAdd or update one entity in the collection
upsertManyAdd or update multiple entities in the collection

Creating and Using Entity Selectors

The entity adapter provides built-in selectors through the getSelectors method:

typescript
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { userAdapter, UserState } from './user.state';

// Create feature selector
const selectUserState = createFeatureSelector<UserState>('users');

// Get the selectors
const {
selectIds,
selectEntities,
selectAll,
selectTotal
} = userAdapter.getSelectors(selectUserState);

// Create custom selectors
export const selectAllUsers = selectAll;
export const selectUserEntities = selectEntities;
export const selectUsersTotal = selectTotal;

export const selectSelectedUserId = createSelector(
selectUserState,
(state) => state.selectedUserId
);

export const selectSelectedUser = createSelector(
selectUserEntities,
selectSelectedUserId,
(entities, selectedId) => selectedId ? entities[selectedId] : null
);

export const selectUsersLoading = createSelector(
selectUserState,
(state) => state.loading
);

Real World Example: Managing a User List

Let's put everything together in a complete example. First, we'll define our actions:

typescript
// user.actions.ts
import { createAction, props } from '@ngrx/store';
import { User } from '../models/user.model';

export const loadUsers = createAction('[User] Load Users');
export const loadUsersSuccess = createAction(
'[User] Load Users Success',
props<{ users: User[] }>()
);
export const loadUsersFailure = createAction(
'[User] Load Users Failure',
props<{ error: string }>()
);

export const addUser = createAction(
'[User] Add User',
props<{ user: User }>()
);
export const addUserSuccess = createAction(
'[User] Add User Success',
props<{ user: User }>()
);

export const updateUser = createAction(
'[User] Update User',
props<{ user: User }>()
);
export const updateUserSuccess = createAction(
'[User] Update User Success',
props<{ user: User }>()
);

export const deleteUser = createAction(
'[User] Delete User',
props<{ id: number }>()
);
export const deleteUserSuccess = createAction(
'[User] Delete User Success',
props<{ id: number }>()
);

export const selectUser = createAction(
'[User] Select User',
props<{ id: number }>()
);

Then, implement the effects to handle API calls:

typescript
// user.effects.ts
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
import { catchError, map, mergeMap } from 'rxjs/operators';
import { UserService } from '../services/user.service';
import * as UserActions from '../actions/user.actions';

@Injectable()
export class UserEffects {
loadUsers$ = createEffect(() =>
this.actions$.pipe(
ofType(UserActions.loadUsers),
mergeMap(() =>
this.userService.getUsers().pipe(
map(users => UserActions.loadUsersSuccess({ users })),
catchError(error => of(UserActions.loadUsersFailure({ error: error.message })))
)
)
)
);

addUser$ = createEffect(() =>
this.actions$.pipe(
ofType(UserActions.addUser),
mergeMap(({ user }) =>
this.userService.addUser(user).pipe(
map(savedUser => UserActions.addUserSuccess({ user: savedUser }))
)
)
)
);

updateUser$ = createEffect(() =>
this.actions$.pipe(
ofType(UserActions.updateUser),
mergeMap(({ user }) =>
this.userService.updateUser(user).pipe(
map(() => UserActions.updateUserSuccess({ user }))
)
)
)
);

deleteUser$ = createEffect(() =>
this.actions$.pipe(
ofType(UserActions.deleteUser),
mergeMap(({ id }) =>
this.userService.deleteUser(id).pipe(
map(() => UserActions.deleteUserSuccess({ id }))
)
)
)
);

constructor(
private actions$: Actions,
private userService: UserService
) {}
}

Now, we can use these in a component:

typescript
// user-list.component.ts
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { User } from '../models/user.model';
import * as UserActions from '../actions/user.actions';
import * as fromUsers from '../reducers/user.selectors';

@Component({
selector: 'app-user-list',
template: `
<div *ngIf="loading$ | async">Loading...</div>
<div *ngIf="error$ | async as error" class="error">{{ error }}</div>

<div>
<h2>Users</h2>
<button (click)="addNewUser()">Add New User</button>

<ul>
<li *ngFor="let user of users$ | async"
[class.selected]="user.id === (selectedUserId$ | async)"
(click)="selectUser(user.id)">
{{ user.name }} ({{ user.email }})
<button (click)="updateUser(user)">Edit</button>
<button (click)="deleteUser(user.id)">Delete</button>
</li>
</ul>
</div>

<div *ngIf="selectedUser$ | async as user">
<h3>Selected User Details</h3>
<p>ID: {{ user.id }}</p>
<p>Name: {{ user.name }}</p>
<p>Email: {{ user.email }}</p>
</div>
`
})
export class UserListComponent implements OnInit {
users$: Observable<User[]>;
selectedUser$: Observable<User | null>;
selectedUserId$: Observable<number | null>;
loading$: Observable<boolean>;
error$: Observable<string | null>;

constructor(private store: Store) {
this.users$ = this.store.select(fromUsers.selectAllUsers);
this.selectedUser$ = this.store.select(fromUsers.selectSelectedUser);
this.selectedUserId$ = this.store.select(fromUsers.selectSelectedUserId);
this.loading$ = this.store.select(fromUsers.selectUsersLoading);
this.error$ = this.store.select(state => state.users.error);
}

ngOnInit(): void {
this.store.dispatch(UserActions.loadUsers());
}

selectUser(id: number): void {
this.store.dispatch(UserActions.selectUser({ id }));
}

addNewUser(): void {
const newUser: User = {
id: Date.now(), // temporary ID, will be replaced by backend
name: 'New User',
email: '[email protected]'
};
this.store.dispatch(UserActions.addUser({ user: newUser }));
}

updateUser(user: User): void {
const updatedUser = {
...user,
name: `${user.name} (edited)`
};
this.store.dispatch(UserActions.updateUser({ user: updatedUser }));
}

deleteUser(id: number): void {
this.store.dispatch(UserActions.deleteUser({ id }));
}
}

Advanced Entity Features

Custom ID Field

If your entity doesn't use id as the primary key, you can specify a different field:

typescript
export const taskAdapter = createEntityAdapter<Task>({
selectId: (task: Task) => task.taskId, // Using taskId as the primary key
});

Custom Sort Function

You can define how entities are sorted:

typescript
export const productAdapter = createEntityAdapter<Product>({
sortComparer: (a: Product, b: Product) => {
// Sort by price descending
return b.price - a.price;
},
});

Working with Non-Normalized Data

Sometimes your API may return non-normalized data. You can use the @ngrx/entity adapter methods to normalize it:

typescript
// API returns nested data
interface OrderResponse {
id: number;
customer: {
id: number;
name: string;
};
items: Array<{
id: number;
name: string;
price: number;
}>;
}

// In your effect:
loadOrders$ = createEffect(() =>
this.actions$.pipe(
ofType(OrderActions.loadOrders),
mergeMap(() =>
this.orderService.getOrders().pipe(
map(ordersResponse => {
// Extract customers and items into their own collections
const customers = ordersResponse.map(order => order.customer);
const items = ordersResponse.flatMap(order => order.items);

// Normalize the orders
const orders = ordersResponse.map(order => ({
id: order.id,
customerId: order.customer.id,
itemIds: order.items.map(item => item.id)
}));

return OrderActions.loadOrdersSuccess({
orders,
customers,
items
});
})
)
)
)
);

Common Patterns and Best Practices

Use Entity Dictionary for Lookups

When you need to look up entities by ID, use the entities dictionary rather than filtering arrays:

typescript
// Good - O(1) lookup
const user = userEntities[userId];

// Avoid - O(n) lookup
const user = users.find(u => u.id === userId);

Batch Updates

For multiple updates, use updateMany instead of multiple updateOne calls:

typescript
// Good
userAdapter.updateMany([
{ id: 1, changes: { active: false } },
{ id: 2, changes: { active: false } },
{ id: 3, changes: { active: false } }
], state);

// Avoid
let newState = userAdapter.updateOne({ id: 1, changes: { active: false } }, state);
newState = userAdapter.updateOne({ id: 2, changes: { active: false } }, newState);
newState = userAdapter.updateOne({ id: 3, changes: { active: false } }, newState);

Keep Entity State Focused

Store only entity data in the entity state. Keep UI-related state separate:

typescript
// Good
interface UserState extends EntityState<User> {
selectedUserId: number | null;
loading: boolean;
}

// Avoid
interface User {
id: number;
name: string;
isSelected: boolean; // UI state doesn't belong in the entity
isExpanded: boolean; // UI state doesn't belong in the entity
}

Summary

NgRx Entity is a powerful extension to NgRx that simplifies the management of collections in your Angular applications. It provides:

  • A standardized way to store collections of entities
  • Utility functions for CRUD operations
  • Performance optimizations through normalized state
  • Built-in selectors for common query patterns

By using NgRx Entity, you can significantly reduce the boilerplate code needed to manage collections while ensuring good performance and maintainability.

Additional Resources

Exercises

  1. Create an entity adapter for a Product entity with fields id, name, price, and category.
  2. Implement a feature that allows filtering products by category using entity selectors.
  3. Add functionality to sort products by price (ascending and descending).
  4. Create a shopping cart feature that uses NgRx Entity to track items in the cart, with the ability to change quantities.
  5. Extend the user management example to include user roles and permissions.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)