This is the second article of the series about @ngrx/entity.
- Managing Your Collections With EntityAdapter
- Multiple Entity Collections in the Same Feature State
- 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
Person
s. 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 {
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 = ({ uid }) => uid;
const sortComparer: Comparer = (p1, p2) => p1.firstName.localeCompare(p2.firstName);
// EntityAdapter
const personAdapter = createEntityAdapter({ 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 Team
s 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 & { editingId: string };
teams: EntityState;
}
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();
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('...');
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!