Did you know about Entity State Adapter in NgRx?

More on how to efficiently and safely manage huge collection of records in NgRx store using Entity State Adapter in angular

Discussion

  • What is EntityStateAdapter?

  • More discussion on why we need to use it.

  • Implementing Entity, EntityState and EntityAdapter with NgRx.

  • conclusion

We use NgRx to maintain the state of our application. When we are building an application that does intensive modification to a collection of records let's say we are dealing with a UI which has a table in it and we modify the data many times. To handle the state of this kind of application we use EntityStateAdapter.

EntityStateAdapter is an API provided to manipulate and query the entities. In our example, each entity is a row in the table. Using this library we get a set of methods using which we can get information about the entities and modify the entities in a typesafe and uniform way. This makes our NgRx store maintainable and easy to do modifications.

Let's understand how to implement EntityStateAdapter on the NgRx store which is a common use-case. For example consider a UI where we display the details of some users the table shows the first name, lastname and user_handle in the table, now each row here is the entity we want to focus on.

export interface User { // Our User interface
  id: number;
  firstname: string;
  lastname: string;
  handle: string;
}

I have already created an application with a NgRx store defined with proper actions, reducer and selectors, our state interface looks like below

// State of the User Feature store which is linked to parent root store.
export interface UserState {
  users: EntityUserState;
}
// State of the users Entity Adapter
export interface EntityUserState extends EntityState<User> {}

Let's understand more about EntityState. Generally, when we are storing an array of objects in a store we tend to create an array with the key users of type User[] and when we want to perform CRUD operation do it by using push, pop, map and filter methods also we might need to sort the items based on some keys.

By writing the login on our own we might be repeating the same logic many times or sometimes we may even try to mutate the objects this does not trigger the change detection cycle in angular / Ngrx to avoid this we can use Entity Adapters which will provide us with a set of methods using which we can safely perform operations on the collection of records and be sure that every time a new and modified state is returned. This state can be stored as a JSON or in localStorage or serialized when needed.

Entity, EntityState and Adapter

  • Entity: In our example, the Entity is the user we create an interface as per our requirements

  • EntityState: This is the state that is returned when we perform some operations on the existing state, The EntityState contains 2 field

    • ids - an array of unique_key that we have to define.

    • entities - The dictionary that has each key is the unique ID mapped to the record/entity that contains the key.

export interface EntityState<T> {
    ids: string[] | number[];
    entities: Dictionary<T>;
}

To create an EntityState we have extended our custom entity state with EntityState as below

export interface EntityUserState extends EntityState<User> {}
  • Adapter - We create an adapter based on the entity state. An adapter is created using createEntityAdapter<T>() now for this function we pass an object as a parameter with 2 keys,

    • selectId - Indicating in each record object, which field/key we want to make as a primary key to identify the records.

    • sortComparer - Indicating the key by which we want to sort the records.

In our example we created the adapter using below code

// users adapter with handle as primary key
const usersAdapter: EntityAdapter<User> = createEntityAdapter<User>({
  selectId: (user: User) => user.handle,
});

Now finally this adapter we can plug it in our state and reducer and use it as below

export function usersReducer(
  state: UserState = initialUserState,
  action: UserActions
): UserState {
  switch (action.type) {
    case UserActionTypes.ADD_USER: {
      return {
        ...state,
        users: usersAdapter.addOne(action.user, state.users),
      };
    }
    case UserActionTypes.LIST_USERS: {
      return state;
    }
    case UserActionTypes.DELETE_USER: {
      return {
        ...state,
        users: usersAdapter.removeOne(action.user.handle, state.users),
      };
    }
    default: {
      return state;
    }
  }
}

In the above code we can see for each action case that state contains users which is using usersAdapter to update itself each adapter function requires 2-3 parameters based on the operation it performs the last parameter is always the current state and each function returns a new modified object ( No mutation ).

This updated state is sent into selectors and we can perform slicing throught the objects, manipulate the data and send it to the components.

In this way we can easily maintain a collection of records and also using this adapters we can create multiple instances of feature store using same code, for complete working application visit this github link

ngrx-entity-state-adapter