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.

In this article:

yb
Yannick Baron is architecture consultant at Thinktecture and focuses on Angular and RxJS.

This is the second 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

Headsup

If you have followed the series, you should be familiar with the EntityState interface. It describes how the EntityAdapter expects our collection to be stored. In the previous articles, we elaborated in detail on how to handle single and multiple collections.
For this example, we simply make EntityState the state of our component and extend it with a couple of common properties. But everything shown before, when we need to handle multiple entity types, applies here as well!

EntityAdapter and feature states

Let’s pick up the example from the first part in this series. A collection of Persons. But this time, instead of managing only a collection of persons, we look at the state of a PersonModule. So in addition to the collection, we store a property editingId, which would hold the id of the person that we are currently editing, just for the sake of this example:
				
					interface Person {
    uid: string;
    firstName: string;
    lastName: string;
}

interface PersonModuleState extends EntityState<Person> {
    editingId: string;
}
				
			
Now, let’s create the corresponding EntityAdapter as we have done before:
				
					// [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 });
				
			
In order to create a reducer, we will need an initial state to fulfill the PersonModuleState interface. 
As seen before, we can make use of the getInitialState() method of personAdapter. This will set up the collection-related fields. However, in this example, we have an additional field, which we also need to provide an initial value for:
				
					const initialState: PersonModuleState = personAdapter.getInitialState({ editingId: '' });
				
			
As we can see we can provide an object that initializes the additional fields in our state.

 

Let’s put this into our reducer and illustrate simple CRUD operations:
				
					const personReducer = createReducer(
  initialState,
  on(PersonAction.add, (state, { person }): PersonModuleState => {
    return personAdapter.addOne(person, state);
  }),
  on(PersonAction.update, (state, { update }): PersonModuleState => {
    return personAdapter.updateOne(update, state);
  }),
  ...
);
				
			
Making use of the EntityAdapter helps keep our reducers that manage our collection very clean! But so far this might not be news to you.

Multiple collections in the state

Now in addition to the Person collection, we will also need a collection for Teams to be able to assign a person their team:
				
					interface Team {
    id: string;
    name: string;
}

interface Person {
    uid: string;
    firstName: string;
    lastName: string;
    teamId: string;
}
				
			
Please note, hat we added the property teamId to the Person interface.

 

We made the decision that the Team model is also part of our PersonModule and thus the collection of teams should be included in the PersonModuleState. In order to achieve this, we have to adjust the interface as such:
				
					interface PersonModuleState {
    persons: EntityState<Person> & { editingId: string };
    teams: EntityState<Team>;
}
				
			
Instead of having the module’s state extend the EntityState interface, we just have two properties of type EntityState. It could be argued where to put the editingId property, if it should be part of the persons state or if it can be kept in the module’s state.

 

Changing the interface also changes how we initialize our state, but it is very similar to before:
				
					const teamAdapter = createEntityAdapter<Team>();

const initialState: PersonModuleState = {
    persons: personAdapter.getInitialState({ editingId: '' }),
    teams: teamAdapter.getInitialState(),
};
				
			
With this in place, we only have to look at the reducers. All EntityAdapter operations require a state to be handed to them. This allows for our substate structure above. Let’s adjust our reducer:
				
					const personReducer = createReducer(
  initialState,
  on(PersonAction.add, (state, { person }): PersonModuleState => {
    const persons = personAdapter.addOne(person, state.persons);
    return { ...state, persons };
  }),
  on(PersonAction.update, (state, { update }): PersonModuleState => {
    const persons = personAdapter.updateOne(update, state.persons);
    return { ...state, persons };
  }),
  // ...
);
				
			
And that is all there is to managing multiple collections in the same state. The way the EntityAdapter is designed, it only operates on the EntityState interface. This allows us to control where we want to keep the collection we manage.

 

Only when using the selectors we have to make a slight adjustment. When using selectors, we also need to tell the EntityAdapter where to find the EntityState its selectors are operating on. For that, we can create a selector which we feed into the getSelectors() method:
				
					const selectPersonModuleState = createFeatureSelector<PersonModuleState>('...');

const selectPersonsState = createSelector(selectPersonModuleState, ({ persons }) => persons);
const selectTeamsState = createSelector(selectPersonModuleState, ({ teams }) => teams);

const personSelectors = personAdapter.getSelectors(selectPersonsState);
const teamSelectors = teamAdapter.getSelectors(selectTeamsState);

const selectAllPersons = personSelectors.selectAll;
const selectAllTeams = teamSelectors.selectAll;
				
			

Conclusion

Above, I illustrated how you can manage several collections in the same feature module. It is as simple as using two sub-states in your feature state, resulting in a structure as such:
 
				
					{
  persons: {
    ids: [],
    entities: {},
    editingId: '',
  },
  teams: {
    ids: [],
    entities: {},
  },
};
				
			
The EntityAdapter is a well-made abstraction with no real dependencies. This inversion of control principle allows us to make use of it freely in multiple contexts.

 

In the next part of the series, I will show you that using the @ngrx/entity package will therefore also work with the ComponentStore!
Free
Newsletter

Current articles, screencasts and interviews by our experts

Don’t miss any content on Angular, .NET Core, Blazor, Azure, and Kubernetes and sign up for our free monthly dev newsletter.

EN Newsletter Anmeldung (#7)
Related Articles
.NET
KP-round
.NET 8 brings Native AOT to ASP.NET Core, but many frameworks and libraries rely on unbound reflection internally and thus cannot support this scenario yet. This is true for ORMs, too: EF Core and Dapper will only bring full support for Native AOT in later releases. In this post, we will implement a database access layer with Sessions using the Humble Object pattern to get a similar developer experience. We will use Npgsql as a plain ADO.NET provider targeting PostgreSQL.
15.11.2023
.NET
KP-round
Originally introduced in .NET 7, Native AOT can be used with ASP.NET Core in the upcoming .NET 8 release. In this post, we look at the benefits and drawbacks from a general perspective and perform measurements to quantify the improvements on different platforms.
02.11.2023
.NET
KP-round
.NET 8 introduces a new Garbage Collector feature called DATAS for Server GC mode - let's make some benchmarks and check how it fits into the big picture.
09.10.2023