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
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 {
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 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 & { 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!
