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 diesem Artikel:

yb
Yannick Baron ist Architekturberater bei Thinktecture mit einem Fokus auf Angular und 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!
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
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

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.
31.01.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
Angular
yb

[Outdated]Configuring Lazy Loaded Angular Modules

Making our Angular modules configurable is an important step in building a reusable architecture. Having used Angular for a while you might be familiar with the commonly used forRoot() and forChild() functions, that some modules provide you with. But what is the best way to provide configuration in these cases?
16.06.2022