Ngrx Entity and Selectors. The why and how
Muhammad Muhktar Musa
Posted on September 19, 2021
Using Ngrx to build an application? The problem of deciding the format of storing data inside the store is always there. Handling of the business data in a centralized store is something that will need to be done in the application. This process could be repetitive and time consuming. It could lead to handwriting the same reducer logic and selectors for different data types. This can be error prone and slows down the development process. We will cover how to solve this problems using the Ngrx Entity and selectors to improve an application.
Entity
Ngrx Entity helps in handling business data in a store. It represents some sort of business data or user interface state. An entity is defined as a typescript custom object type, like in the example below
export interface EarlyconfigState <earlyConfig> {
// additional entities state properties
selectedId: number;
loading: boolean;
error: string;
query: EarlyconfigSearchQuery;
}
As can be seen, the entity has a unique identifier field called selectedId. This unique id can be either a string or a number. It is a technical identifier that is unique to a given instance of the entity. Most data stored in a store are entities.
EntityState, EntityAdapter, createEntityAdapter
Ngrx Entity is a small library that helps keep entities in an ideal entity state format. It is designed to be used in conjunction with the Ngrx store and it is a key part of Ngrx ecosystem. Let us take a deeper look to see how entity helps in writing an Ngrx application.
export interface EarlyconfigState extends EntityState<earlyConfig> {
// additional entities state properties
selectedId: number;
loading: boolean;
error: string;
query: EarlyconfigSearchQuery;
}
From the example code above we inherit our properties from EntityState. It is imported from the @ngrx/store
. The EntityState makes it easier to manipulate entities.
To use the EntityState and other features of Ngrx Entity, an EntityAdapter needs to be created. This adapter is a utility class that provides a series of utility functions that are designed to make it simple to manipulate entity state. The adapter allows all initial entity state to be written. Let us take a look at it in action
export const EarlyconfigAdapter: EntityAdapter<earlyConfig> = createEntityAdapter<earlyConfig>();
The adapter can now be taken and used to define the initial state that will be needed for the reducers. This reducers will be implemented like in the code below.
export const initialEarlyconfigState: EarlyconfigState = EarlyconfigAdapter.getInitialState({
// additional earlyconfig state properties
selectedId: null,
loading: false,
error: '',
query: {
filter: '',
sorting: '',
limit: 999,
page: 1
}
});
The adapter in turn can be used to write reducer functions and logic.
export function EarlyconfigReducer(state = initialEarlyconfigState, action: EarlyconfigActions): EarlyconfigState {
switch (action.type) {
case EarlyconfigActionTypes.CreateEarlyconfig:
return {
...state,
loading: true,
error: ''
};
case EarlyconfigActionTypes.CreateEarlyconfigSuccess:
return {
...EarlyconfigAdapter.addOne(action.payload.result, state),
loading: false,
error: ''
};
default:
return state;
}
}
Benefits of Entity Adapter
Using adapters to write reducers spares a lot of work and helps in avoiding common reducer logic bugs. The adapter prevents the problem of mutating the store state, as well as reducing the amount of code needed to write reducers to the barest minimum.
Ngrx Entity helps with the commonly needed selectors. This selectors are then ready to be used directly in a component or as a starting point for building other selectors. Though the ngrx Entity allows the writing of state, reducer and selector logic to be easier, the reducer function itself still has to be written. Using the Ngrx Entity does not avoid having to write reducer logic for each entity. It makes it simpler.
The convention to be followed is to pull all the closely related code that uses the adapter directly into the same file where the entity reducer function is defined.
export function EarlyconfigReducer(state = initialEarlyconfigState, action: EarlyconfigActions): EarlyconfigState {
switch (action.type) {
case EarlyconfigActionTypes.CreateEarlyconfig:
return {
...state,
loading: true,
error: ''
};
case EarlyconfigActionTypes.CreateEarlyconfigSuccess:
return {
...EarlyconfigAdapter.addOne(action.payload.result, state),
loading: false,
error: ''
};
case EarlyconfigActionTypes.SearchAllEarlyconfigEntities:
return {
...EarlyconfigAdapter.removeAll(state),
loading: true,
error: ''
};
case EarlyconfigActionTypes.SearchAllEarlyconfigEntitiesSuccess:
return {
...EarlyconfigAdapter.setAll(action.payload.result, state),
loading: false,
error: ''
};
case EarlyconfigActionTypes.SearchAllEarlyconfigEntitiesFail:
return {
...state,
loading: false,
error: 'Earlyyearsconfig search failed: ' + action.payload.error
};
default:
return state;
}
}
In practice each entity has slightly different reducer logic so
that there would be no code repetition between reducer functions.
In conclusion, Ngrx Entity is an extremely useful package. In order to understand it, it is essential to first be familiar with the base store concepts like Actions, Reducers and effects. You can find lessons using the link below
Link understanding-ngrx-actions-reducers-and-effects
Ngrx Entity is designed to handle only the business entities in a store making it simple to store such entities in memory in a convenient way.
Selectors
Selectors are pure functions used for obtaining slices of store state. Ngrx store provides some helper functions for optimizing this selection. When selecting slices of state, selectors provide many features like
- Portability
- Memoization
- Composition
- Testability
- Type safety
createSelector function
When using the createSelector function, the Ngrx store keeps track of the latest arguments in which a selector function was invoked. The last result can be returned when the arguments match without revoking a selector function. This is possible because of the nature of selectors which are pure functions.
This provides performance benefits especially with functions that perform expensive computation.
The createSelector function can be used to select some data from the same state based on several slices of the same state. let us take a look an example
export const getSelectedId = (state: EarlyconfigState) => state.selectedId;
export const getLoading = (state: EarlyconfigState) => state.loading;
We are getting our state from the reducer file. We have a getSelectedId and getLoading object. The createSelector method creates a result that filters some of the state by another section of the state and brings the state up to date.
The selector looks like this
export const {
selectIds: EarlyconfigIds,
selectEntities: EarlyconfigEntities,
} = EarlyconfigAdapter.getSelectors(getEarlyConfigState);
export const currentEarlyconfigId = createSelector(
getEarlyConfigState,
getSelectedId
);
export const currentEarlyconfigIds = createSelector(
getEarlyConfigState,
EarlyconfigIds
);
The selector function returned by calling createSelector initially has a memoized value of null. After a selector is invoked the first time its value is stored in memory. If the selector is subsequently invoked with the same arguments, it will re-compute and update its memoized value. A selectors memoized value stays in memory indefinately. If the memoized value is a large dataset that is no longer needed, it is then possible to reset the memoized value to null so that the large dataset can be removed from memory. This is achieved by invoking the release method on the selector.
currentEarlyConfigId.release();
Posted on September 19, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.