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_300x300
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
pg
While basic value objects solve primitive obsession, complex domain requirements need sophisticated modeling techniques. This article explores advanced patterns using Thinktecture.Runtime.Extensions to tackle real-world scenarios: open-ended dates for employment contracts, composite file identifiers across storage systems, recurring anniversaries without year components, and geographical jurisdictions using discriminated unions.
19.10.2025
.NET
pg
Domain models often involve concepts that exist in multiple distinct states or variations. Traditional approaches using enums and nullable properties can lead to invalid states and scattered logic. This article explores how discriminated unions provide a structured, type-safe way to model domain variants in .NET, aligning perfectly with Domain-Driven Design principles while enforcing invariants at the type level.
06.10.2025
.NET
pg
Learn how to seamlessly integrate Smart Enums with essential .NET frameworks and libraries. This article covers practical solutions for JSON serialization, ASP.NET Core model binding for both Minimal APIs and MVC controllers, and Entity Framework Core persistence using value converters. Discover how Thinktecture.Runtime.Extensions provides dedicated packages to eliminate integration friction and maintain type safety across your application stack.
21.09.2025