This is the first article of the series about @ngrx/entity.
- Managing Your Collections With EntityAdapter
- Multiple Entity Collections in the Same Feature State
- 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 & { 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 {
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 functionto create such an adapter and start managing our collection:
// [optional] Id Accessor and Comparer (sort by firstName ascending)
const selectId: IdSelector = ({ uid }) => uid;
const sortComparer: Comparer = (p1, p2) => p1.firstName.localeCompare(p2.firstName);
// EntityAdapter
const personAdapter = createEntityAdapter({ selectId, sortComparer });
// Creating the collection
let collection: EntityState = personAdapter.getInitialState();
// => { ids: [], entities: {} }
Using the
createEntityAdapter
method we create an EntityAdapter
for our person entity. In theabove 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
thenproviding 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 secondparameter. The
EntityAdapter
also follows the immutability principle, and as you can see, it has nodependencies 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 = {
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
islast 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 findthem documented here: https://ngrx.io/guide/entity/adapter#adapter-collection-methods.
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 ourcollection. 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 seethat managing a collection can easily be achieved using the
EntityAdapter
. Due to not having anydependencies 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
withthe
@ngrx/store
as well as the @ngrx/component-store
!