16 - Entity State Adapter

In this section we add entity adapter

1. npm install entity from NgRx

In the past you had to install the @ngrx/entity package separately. Now, you might have noticed in step 13 when we generated our store with the @nrwl/angular:ngrx schematic, it was added automatically. Here is a brief overview of the Entity package and why we use it.

The store is like an in-memory database, and the Entity class gives a unique identifier to each object similar to a primary key. In this way objects can then be flattened out and linked together using the entity unique identifiers, just like in a database. This makes it simpler to combine the multiple entities and 'join' them via a selector query.

The Entity State format consists of the objects in a map called "entities", and store the order of the objects in an array of ids.

The Adapter is a utility class that provides a series of functions designed to make it really simple to manipulate the entity state, such as changing the order.

You can find out more by reading this Angular University piece on the subject.

2. Use entity adapter in reducer

  • Extend ProductState with EntityState. By default it will make an entities and ids dictionary. You Add to this any state properties you desire.

  • Create an adapter and use it's getInitialState method to make the initial state

libs/products/src/lib/+state/products.reducer.ts
import { createReducer, on, Action } from '@ngrx/store';
import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';
import * as ProductsActions from './products.actions';
import { Product } from '@demo-app/data-models';

export const PRODUCTS_FEATURE_KEY = 'products';

export interface ProductsData extends EntityState<Product> {
  selectedProductId?: string | number;
  loading: boolean;
  error?: string | null;
}

export interface ProductsState {
  readonly products: ProductsData;
}

export const productsAdapter: EntityAdapter<Product> = createEntityAdapter<Product>({});

export interface ProductsState {
  readonly products: ProductsData;
}

export const productsAdapter: EntityAdapter<Product> = createEntityAdapter<Product>({});

export const initialState: ProductsData = productsAdapter.getInitialState({
  error: '',
  selectedProductId: null,
  loading: false,
});
/// Abbreviated

3. Update the default reducer logic

Adapter Collection Methods

The entity adapter also provides methods for operations against an entity. These methods can change one to many records at a time. Each method returns the newly modified state if changes were made and the same state if no changes were made.

  • addOne: Add one entity to the collection

  • addMany: Add multiple entities to the collection

  • setAll: Replace current collection with provided collection

  • setOne: Add or Replace one entity in the collection.

  • setMany: Add or Replace multiple entities in the collection

  • removeOne: Remove one entity from the collection

  • removeMany: Remove multiple entities from the collection

  • removeAll: Clear entity collection

  • updateOne: Update one entity in the collection

  • updateMany: Update multiple entities in the collection

  • upsertOne: Add or Update one entity in the collection

  • upsertMany: Add or Update multiple entities in the collection

  • mapOne: Update one entity in the collection by defining a map function

  • map: Update multiple entities in the collection by defining a map function, similar to Array.map

https://ngrx.io/guide/entity/adapter

libs/products/src/lib/+state/products.reducer.ts
export const productsReducer = createReducer(
  initialState,
  on(ProductsActions.loadProducts, (state) => ({
    ...state,
    loading: false,
  })),
  on(ProductsActions.loadProductsSuccess, (state, { payload: products }) => {
    return productsAdapter.setAll(products, state);
  }),
  on(ProductsActions.loadProductsFailure, (state, { error }) =>
    productsAdapter.removeAll({
      ...state,
      error,
    })
  )
);

export function reducer(state: State | undefined, action: Action) {
  return productsReducer(state, action);
}

4. Add selector methods to bottom of reducer

libs/products/src/lib/+state/products.reducer.ts
export const getSelectedProductId = (state: ProductsData) =>
  state.selectedProductId;

export const {
  // select the array of user ids
  selectIds: selectProductIds,

  // select the dictionary of Products entities
  selectEntities: selectProductEntities,

  // select the array of Products
  selectAll: selectAllProducts,

  // select the total Products count
  selectTotal: selectProductsTotal
} = productsAdapter.getSelectors();

5. Add default selectors to use entity adapter

libs/products/src/lib/+state/products.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { ProductsData } from './products.reducer';
import * as fromProduct from './products.reducer';

const getProductsState = createFeatureSelector<ProductsData>('products');

const getProducts = createSelector(
  getProductsState,
  fromProduct.selectAllProducts
);
const getProductEntities = createSelector(
  getProductsState,
  fromProduct.selectProductEntities
);
const getSelectedProductId = createSelector(
  getProductsState,
  fromProduct.getSelectedProductId
);
const getSelectedProduct = createSelector(
  getProductEntities,
  getSelectedProductId,
  (productsDictionary, id) => {
    return productsDictionary[id];
  }
);

export const productsQuery = {
  getProducts,
  getProductEntities,
  getSelectedProductId,
  getSelectedProduct,
};

6. Examine State tree in Devtools

At this point, if you run the app however, you might get an error like this:

Error: libs/products/src/lib/containers/products/products.component.ts:21:5 - error TS2322: Type 'Observable<ProductsEntity[]>' is not assignable to type 'Observable<Product[]>'.
  Type 'ProductsEntity[]' is not assignable to type 'Product[]'.
    Type 'ProductsEntity' is missing the following properties from type 'Product': name, category
21     this.products$ = this.store.pipe(select(productsQuery.getProducts));

The app is not going to run now without updating the products component. This will be fixed in the next step.

Last updated