Managing Your Collections With the EntityAdapter: @ngrx/entity-Series – Part 1

This three-part series of blogposts is targeted at developers who have already gained experience with NgRx but still manage their collections themselves. In the first part I introduce the Entity Adapter, in the second part I show you how to connect it to NgRx and in the third part how to do it with the Component Store as well.

In diesem Artikel:

yb
Yannick Baron ist Architekturberater bei Thinktecture mit einem Fokus auf Angular und RxJS.

This is the first article of the series about @ngrx/entity.

  1. Managing Your Collections With EntityAdapter
  2. Multiple Entity Collections in the Same Feature State
  3. Using EntityAdapter With ComponentStore

A complaint the NgRx team and all proponents of the NgRx Store have heard at least once, is the one of boilerplate. The team behind NgRx is creating new ways and abstractions to reduce the boilerplate needed for common operations in our projects. In the following I want to highlight the NgRx Entity package which helps with managing a collection of entities.

Managing collections immutably

For the sake of this example let’s create an interface for a Person entity:
				
					interface Person {
    uid: string;
    firstName: string;
    lastName: string;
}
				
			
Typically, when we manage a small collection immutably, we simply make use of a JavaScript array:
				
					let collection: Person[] = [];
				
			
After creating a new object fulfilling the Person interface, we can easily add to our collection:
				
					const newPerson: Person = { uid: 'person-1', firstName: 'Yannik', lastName: 'Baron' };

collection = [...collection, newPerson];
				
			
Ideally, if we know the id property of our entity, we filter out the element with the matching id first in order to avoid duplicates.
 
Updating an entity in place is as simple as mapping the array:
				
					const update: Partial<Person> & { uid: string } = { uid: 'person-1', firstName: 'Yannick' };

collection = collection.map(person => person.uid !== update.uid ? person : { ...person, ...update });
				
			
If you are managing a collection of entities of any kind in your app and follow the immutability principles, you have probably written similar lines of code several times. The operations we want to perform on collections are usually our standard CRUD operations, like adding, inserting, upserting, removing, etc. 
These operations are very simple to implement and follow pretty much the same structure.
 
Collections also have one property that leads us to think of an array or list data structure when implementing. That is the property of ordering. The elements in a collection are usually ordered and ideally, we make sure that we keep that order when inserting or updating entities.

Collections as lists

In computer science, we know that managing a collection in a simple list structure is not
always the most performant way for huge collections and thus other data structures have been born.
Without going too much into detail, as performance and runtime are also highly dependent on the
environment and use of the collection, it is clear that if we want to update, remove or access an
entity, we would need to find it in our array first, which comes with looping through the array.

Also, managing a collection, as stated before, results in a very similar implementation each time.
Therefore, an abstraction can be made, and the @ngrx/entity package and its EntityAdapter was born.

Introducing EntityState and EntityAdapter

Desiring direct access to entities via their id properties leads us to a JavaScript object. In order to
clear the requirement of order, we can simply keep a list of the ids in the order we want the entities to be in. This leads us to managing our collection as such:
				
					interface Collection<T> {
    ids: string[];
    entities: { [id: string]: T | undefined };
}
				
			
In case you have not seen the notation of the entities property above, it is a simple JavaScript object
which has strings as propery names and entites of type T as values.
The @ngrx/entity package provides us with an interface doing exactly that, but it is called EntityState<T>.
We can use that interface essentially anywhere we like to store a collection!
Furthermore, we are provided with an abstraction that lets us perform all operations on the collection
and is appropriately known as EntityAdapter. In typical NgRx fashion we can use a creator function
to create such an adapter and start managing our collection:
				
					// [optional] Id Accessor and Comparer (sort by firstName ascending)
const selectId: IdSelector<Person> = ({ uid }) => uid;
const sortComparer: Comparer<Person> = (p1, p2) => p1.firstName.localeCompare(p2.firstName);

// EntityAdapter
const personAdapter = createEntityAdapter<Person>({ selectId, sortComparer });

// Creating the collection
let collection: EntityState<Person> = personAdapter.getInitialState();

// => { ids: [], entities: {} }
				
			
Using the createEntityAdapter method we create an EntityAdapter for our person entity. In the
above example we also provide a function to map an entity to its it property, which in the case
of Person is the property uid . We are also adding a comparer, to sort the entities by firstName
ascending. If we do not care about the order of our entities and the id property is called id then
providing the respective function becomes optional.

Adding to the collection

Following our example above, let’s add a new person to our collection, making use of the personAdapter:
				
					const newPerson: Person = { uid: "person-1", firstName: "Yannik", lastName: "Baron" };

collection = personAdapter.addOne(newPerson, collection);

// {
//   ids: [ 'person-1' ],
//   entities: {
//     'person-1': { uid: 'person-1', firstName: 'Yannik', lastName: 'Baron' }
//   }
// }
				
			
Please note how we reassign the collection and the addOne method expects the collection as the second
parameter. The EntityAdapter also follows the immutability principle, and as you can see, it has no
dependencies and thus can be used essentially anywhere.
But let’s also showcase a couple more operations, like adding multiple persons and updating one:
				
					// Add many
const newPersons: Person[] = [
  { uid: "person-2", firstName: "Mathias", lastName: "Portmann" },
  { uid: "person-3", firstName: "Rafael", lastName: "Schertius" },
];

collection = personAdapter.addMany(newPersons, collection);


// Update
const update: Update<Person> = {
  id: "person-1",
  changes: { firstName: "Yannick" },
};

collection = personAdapter.updateOne(update, collection);

// {
//   ids: [ 'person-2', 'person-3', 'person-1' ],
//   entities: {
//     'person-2': { uid: 'person-2', firstName: 'Mathias', lastName: 'Portmann' },
//     'person-3': { uid: 'person-3', firstName: 'Rafael', lastName: 'Schertius' },
//     'person-1': { uid: 'person-1', firstName: 'Yannick', lastName: 'Baron' }
//   }
// }
				
			
Take note of the order of the ids. As we provided a comparator, the id of the person Yannick is
last in order, as we are ordering by the firstName property.

 

These operations and many more are available out of the box on the personAdapter and you can find

Accessing the collection

In the case illustrated above, we can easily access the respective properties on our collection data structure. However, we should make use of the adapter as we shouldn’t really have to care too much about how the data is stored for us.

 

Using the EntityAdapter’s getSelectors() method, we are provided with selectors to read from our
collection. A selector is a pure mapping function that in our case accepts the collection and returns the
relevant information. This is in line with the principles of the NgRx Store.

 

Let’s select our entities in order and also get the total count:
				
					const { selectAll, selectTotal } = personAdapter.getSelectors();

console.log(selectAll(collection));
// [
//   { uid: 'person-2', firstName: 'Mathias', lastName: 'Portmann' },
//   { uid: 'person-3', firstName: 'Rafael', lastName: 'Schertius' },
//   { uid: 'person-1', firstName: 'Yannick', lastName: 'Baron' }
// ]

console.log(selectTotal(collection));
// 3
				
			
We can see that using the selectAll selector, returns us a list of our entities in order of the ids array.

 

Furthermore, selectors to retrieve our entities object as well as the list of ids are available to us as well:
				
					const { selectEntities, selectIds } = personAdapter.getSelectors();

console.log(selectEntities(collection)['person-1']);
// { uid: 'person-1', firstName: 'Yannick', lastName: 'Baron' }

console.log(selectIds(collection));
// [ 'person-2', 'person-3', 'person-1' ]
				
			

Conclusion

The above post introduces the @ngrx/entity package outside of its use with @ngrx/store. We can see
that managing a collection can easily be achieved using the EntityAdapter. Due to not having any
dependencies we can make use of this collection management utility in various contexts. If we need
to handle multiple collections, we can easily create multiple adapters. Also, the rich API makes
updating our collection fairly simple and we can save a lot of boilerplate implementations in CRUD
heavy applications.

Stay tuned for the next posts in this series where we elaborate on using the EntityAdapter with
the @ngrx/store as well as the @ngrx/component-store!
Kostenloser
Newsletter

Aktuelle Artikel, Screencasts, Webinare und Interviews unserer Experten für Sie

Verpassen Sie keine Inhalte zu Angular, .NET Core, Blazor, Azure und Kubernetes und melden Sie sich zu unserem kostenlosen monatlichen Dev-Newsletter an.

Newsletter Anmeldung
Diese Artikel könnten Sie interessieren
Low-angle photography of metal structure
AI
cl-neu

AI-Funktionen zu Angular-Apps hinzufügen: lokal und offlinefähig

Künstliche Intelligenz (KI) ist spätestens seit der Veröffentlichung von ChatGPT in aller Munde. Wit WebLLM können Sie einen KI-Chatbot in Ihre eigenen Angular-Anwendungen integrieren. Wie das funktioniert und welche Vor- und Nachteile WebLLM hat, lesen Sie hier.
26.02.2024
Angular
SL-rund

Konfiguration von Lazy Loaded Angular Modulen

Die Konfigurierbarkeit unserer Angular-Module ist für den Aufbau einer wiederverwendbaren Architektur unerlässlich. Aber in der jüngsten Vergangenheit hat uns Angular seine neue modullose Zukunft präsentiert. Wie sieht das Ganze jetzt aus? Wie konfigurieren wir jetzt unsere Lazy-Komponenten? Lasst uns gemeinsam einen Blick darauf werfen.
03.08.2023
Angular
yb

Using EntityAdapter with ComponentStore: @ngrx/entity Series – Part 3

As someone who enjoys the ComponentStore on an average level, I have written simple reactive CRUD logic several times. While storing a vast number of entities in the component state might not be a frequent use case, I will briefly illustrate the usage of the EntityAdapter with the @ngrx/component-store.
14.02.2023
Angular
yb

Multiple Entity Collections in the Same Feature State: @ngrx/entity-Series – Part 2

After introducing the @ngrx/entity package, I am often asked how to manage multiple entity types in the same feature state. While I hope that the previous part of this article series has made this more apparent, I will further focus on this question in the following.
07.02.2023
Angular
MS-rund

Implementing Smart and Presentational Components with Angular: Condensed Angular Experiences – Part 4

In this article, we will explore how to apply the concept of smart and presentational components with Angular. We will choose a complex-enough target to see all aspects in action, yet understandable and within the scope of this article. The goal is to teach you how to use this architecture in your way. For that, we will iterate through different development stages, starting with the target selection and implementing it in a naive way. After the first development, we will refactor that naive solution into smart and presentational components that are reusable, refactor-friendly, and testable.
23.01.2023
Angular
SL-rund

Angular OnPush – A Change Detection Strategy Revealing Mistakes in Your Code

When optimizing the performance of Angular applications, many developers directly associate Angular's OnPush change detection strategy with it. But, if you don't know exactly how OnPush works under the hood, it will quickly teach you what you are doing wrong the hard way. In this article, we'll look deeper into how the OnPush strategy affects Angular's change detection mechanism and which pitfalls we should avoid at all costs.
24.10.2022