NgRx Use Cases, part II: Working with Lists
Tags: ngrx, angular, typescript
Published: 2022-11-23 10:35:02

_Original cover photo by [Glenn Carstens-Peters](https://unsplash.com/@glenncarstenspeters) on Unsplash._

In my [previous article](https://dev.to/this-is-angular/ngrx-use-cases-part-i-restricting-access-30lo) we covered implementing authorization using [@ngrx/store](https://ngrx.io/guide/store) and [@ngrx/effects](https://ngrx.io/guide/effects). In this article we will address working with lists of data. Here are some particular use cases we will cover:

- Loading a list of data from the server
- Adding an item to the list
- Updating an item in the list
- Deleting an item from the list
- Retrieving a single item to show

Of course, all of these cases can be implemented "manually" by just creating actions, reducers, and effects. But we will use the [@ngrx/entity](https://ngrx.io/guide/entity) package to make our lives easier.

## Introducing @ngrx/entity

The @ngrx/entity package provides a set of helper functions for working with collections of data. It provides a set of functions for managing the state of a collection of entities, including:

- Adding and removing entities
- Updating entities
- Selecting entities
- Sorting entities
- Filtering entities
- and so on

### How it works?

First of all, let's describe our example. Imagine we have a web application that allows users to view and buy products. We have a list of products that we want to display on the main page. We also have a product details page where we show more information about a single product. We also want to allow users to add products to their shopping cart.

Here is how a product will look like:

```ts
export interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
}
```

But here is a catch. When we retrieve the list of products, we don't get the full information about each product. We only get the product id, name, and price. We have to make a separate request to get the full information about a product. So we will have two different interfaces for a product. This is one way of doing this:

```ts
export interface Product {
  id: number;
  name: string;
  price: number;
  image: string;
}

export interface ProductDetails extends Product {
  description: string;
  tags: string[];
}
```

But this approach has a significant drawback. Problem is, we do **not** want to store a current viewed product detail in the store. We want to store only the list of products, and select one particular product via a selector when we need it. We can do the following to solve this problem:

```ts
type Product = {
  id: number;
  title: string;
  price: number;
  image: string;
  description: string;
  tags: string[];
};

type PartialRequired<
  T,
  K extends keyof T
> = Pick<T, K> & Partial<Omit<T, K>>;

type ProductDetails = PartialRequired<
   Product,
   "title" | "price" | "image"
>;
```

Here, the `PartialRequired` type we created does the heavylifting: it creates a new type that is a combination of the `Product` type and a partial version of the `Product` type. The `PartialRequired` type takes two generic parameters: the first one is the type we want to create a partial version of, and the second one is a union type of the properties we want to make required. So the resulting type will have all the properties of the `Product` type as **optional**, but the `title`, `price`, and `image` properties will be required. Thus, we will store a list of `ProductDetails` in the store, and we will first update a single `Product` to contain additional data for displaying and then select it as a `ProductDetail` when we need it.

Next, let's put this all into the `Store`:

### Defining the state

In `@ngrx/entity` the state of a collection of entities is represented by the `EntityState` interface. Here is how it works:

```ts
// state.ts file
export interface ProductsState extends EntityState<ProductDetails> {}

export const productsAdapter = createEntityAdapter<ProductDetails>(); 
```

`productsAdapter` is an object that contains a set of helper functions for working with the `EntityState`. It provides a set of functions for managing the state of a collection of entities, including adding, removing, updating, selecting, sorting, filtering, and so on. The `createEntityAdapter` function takes a generic type parameter that is the type of the entity we want to work with. In our case, it is the `ProductDetails` type.

### Creating actions

Next, we want to define some actions to handle the state of the products. As we are going to create several related actions, we can use the new [`createActionGroup`](https://ngrx.io/api/store/createActionGroup) function from `@ngrx/store`:

```ts
// actions.ts file
export const ProductsActions = createActionGroup({
  source: 'Products',
  events: {
    'Load All': emptyProps(),
    'Load All Success': props<{products: ProductDetails[]}>(),
  },
});
```

### Creating a reducer for state updates

So far, we have created the state and defined the actions that can modify it. Now let's create a reducer to show exactly how the state can be changed and how the entity adapter helps us to do exactly that:

```ts
// reducer.ts file
// imports omitted

export const productsReducer = createReducer(
  productsAdapter.getInitialState(),
  on(ProductsActions.loadAllSuccess, (state, { products }) =>
    productsAdapter.addMany(products, state)
  ),
);
```

Here we see our `productsAdapter` in action for the first time: we use its built-in `addMany` method to insert the list of products into the state. The `addMany` method takes two parameters: the first one is the list of products we want to insert, and the second one is the current state. The `addMany` method returns a new state with the products inserted. In general, all the methods of the `productsAdapter` work in this fashion, as pure functions that take the previous state and the data to be modified and returning a new instance of our state.

### Using the state 

Now let's address selecting our list of products. `productsAdapter` will contain a bunch of built-in selectors for our list of products:

```ts
// selectors.ts file
const productsFeature = createFeatureSelector<ProductsState>(
  'products',
);

export const selectors = productsAdapter.getSelectors();

export const selectAllProducts = createSelector(
  productsFeature,
  selectors.selectAll
);
```

Now we can simply use this newly created selector in our component:

```ts
// product-list.component.ts file
@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html',
  styleUrls: ['./product-list.component.css'],
})
export class ProductListComponent implements OnInit {
  products$ = this.store.select(fromProducts.selectAllProducts);

  constructor(private readonly store: Store) {}

  ngOnInit() {
    this.store.dispatch(ProductsActions.loadAll());
  }
}
```

As you can see, with an entity adapter we do not really need to write any sort of business logic about the lists of products. We can simply use the built-in selectors and reducers to manage the state of our products. This means greatly reducing boilerplate, but this is only the beginning. Now let's handle displaying details for one product, which will require a bit more data. 

> **Note**: for the sake of brevity we skip the effects part, but we can imagine we have an effect that makes an http call and returns the `ProductActions.loadAllSuccess` action.

### Loading a single entity

Essentially in this part, what we want to do is to have a separate page for product details, and, when the user navigates to that page, we want to load the full information about the product and display it. Here, we can use the [`@ngrx/router-store`](https://ngrx.io/guide/router-store) package to get the current route and the id of the product we want to display. We can then use the `productsAdapter` to select the product from the store:

Router store provides built-in actions and states for router navigation. We are going to use an action from that collection in our case to load details about a single product when the user navigates to `product-details/:id`:

```ts
// effects.ts file
@Injectable()
export class ProductsEffects {
  // other effects omitted

  loadProductDetails$ = createEffect(() => {
    return this.actions$.pipe(
      ofType(routerNavigationAction),
      filter(({payload}) => {
        return payload.event.url.includes('product-details');
      }),
      mergeMap(({payload}) => {
        const id = payload.event.state.root.paramMap.get('id');
        return this.productsService.getProduct(id).pipe(
          map(product => ProductsActions.loadProductDetailsSuccess({
            product,
          })),
          catchError(
            error => of(
              ProductsActions.loadProductDetailsRrror({error}),
            ),
          )
        );
      })
    );
  });

  constructor(
    private readonly actions$: Actions,
    private readonly productService: ProductService,
  ) {}
}
```

The important deal here is the `routerNavigationAction` action imported from `@ngrx/router-store`. This action is dispatched when the user navigates to a new route. We can use the `filter` operator to filter out the actions that do not match our route. In our case, we want to load the product details only when the user navigates to the `product-details/:id` route. 

Next, we are going to do an interesting thing in our reducer. In our case, we want to update an existing product in the state with the full information we have received from the server. So for this, we want to find the product by id and update it. But here is a catch: what if the user navigates to the details page directly by the url, without going through the list of products? In this case, we do not have the product list in the state yet. In this case, we would want to add that one single product with full details, which now would require some messy logic. 

Thankfully, `@ngrx/entity` has got us covered with a special `upsertOne` method:

```ts
// reducer.ts file

export const productsReducer = createReducer(
  productsAdapter.getInitialState(),
  // other handlers ommited
  on(
    ProductsActions.loadProductDetailsSuccess,
    (state, {product}) => productsAdapter.upsertOne(product, state)
  ),
);
```

So what does `upsertOne` do? It finds a product and updates it, or if it does not exist, it adds it to the state. This is exactly what we need in our case, no need to write any heavy logic!

### Selecting a single entity

Now, all is left is to select the correct entity from the store and display it in our component. For this, we are going to use a predefined router selector from `@ngrx/router-store` to select a specific product from the list:

```ts
// selectors.ts file
import { getSelectors } from '@ngrx/router-store';
export const selectEntities = createSelector(
  productsFeature,
  selectors.selectEntities
);

const { selectRouteParams } = getSelectors();

export const selectSingleProduct = createSelector(
  selectEntities,
  selectRouteParams,
  (entities, { id }) => entities[id]
);
```

`selectEntites` selector here returns the dictionary of the products ditrectly, so we can just select one by id. Next we use the `getSelectors` function from `@ngrx/router-store` to get the `selectRouteParams` selector. This selector returns the current route parameters, so we can use it to get the id of the product we want to display.

And finally in the component:

```ts
// product-details.component.ts file
@Component({
  selector: 'app-product-details',
  templateUrl: './product-details.component.html',
  styleUrls: ['./product-details.component.css'],
})
export class ProductDetailsComponent implements OnInit {
  product$ = this.store.select(selectSingleProduct);

  constructor(
    private readonly store: Store,
  ) {}

  ngOnInit() {}
}
```

So what remains is to just display the product details in the template:

> **Note**: with this approach it is possible to skip calling the API every time the user navigates to the details page. We can simply check if the product is already in the store and if it is, we can just select it. **This heavily depends on the actual business logic and should be considered individually, so do not rush to change all your API calls, try to understand whether picking from the store is better than making a call**

### Interconnected states: Adding a product to the cart

In such an app, it is an expected feature to have a cart where the user can add products before checking out. 

A cart may be thought of as an array of product ids, or maybe objects with product ids and quantities. So it might be tempting to just keep an array of such objects, but let's consider a scenario: what if the user wants to add a product into the cart after already having it in there, for example, to change the quantity of items? Also, we would need to see the total price of the cart, and we would need to be able to remove products from the cart.

For this, it would be a better approach to have the cart as another `EntityState` to be able to pull off these manipulations without writing too much boilerplate code. Lets create the state and reducer:
  
```ts
// state.ts file

export interface CartItem {
  productId: number;
  quantity: number;
}

export interface CartState extends EntityState<CartItem> {}
```

Now, in the adapter, we must do something new:

```ts
export const cartAdapter = createEntityAdapter<CartItem>({
  selectId: item => item.productId,
});
```

The `selectId` here is needed to define that we do not want a custom generated `id`, but, rather, we want to use the `productId` as the id of this entity. This is because we want to have only one item per product in the cart entity, and we want to only update the quantity of the item if the user adds the same product again.

Now, we can create the reducer:

```ts
export const cartReducer = createReducer(
  cartAdapter.getInitialState(),
  on(
    CartActions.addToCart,
    (state, {item}) => cartAdapter.upsertOne(item, state),
  ),
  on(
    CartActions.removeFromCart,
    (state, {productId}) => cartAdapter.removeOne(productId, state),
  ),
);
```

Notice that we use `upsertOne` here as well, because, as mentioned, we want to update the quantity of the item if it already exists in the cart, rather than add the new product as a separate entity.

Now, we finally come to the most interesting part. What we want is to create a single selector that will return us the whole data related to the cart, namely, how many items we have, the total price and the list of products with their quantities itself. 

Here we go:

```ts
export const cartSelectors = cartAdapter.getSelectors();

export const selectCartItems = createSelector(
  cartFeature,
  cartSelectors.selectAll
);

export const selectCart = createSelector(
  selectCartItems,
  selectCartTotal,
  selectProductEntities,
  (items, total, entities) => ({
    items: items.map((item) => ({
      ...entities[item.productId],
      quantity: item.quantity,
    })),
    total,
    totalPrice: items.reduce(
      (
        acc,
        next,
      ) => acc + (next.quantity * entities[next.productId].price),
      0
    ),
  })
);
```

Here, we combine the list of all products with the contents of the cart to extract the full information about the cart. We also use the `selectCartTotal` selector that we have created earlier to get the total number of items in the cart. This finalized selector is ready to be used directly in the cart component. Notice two things:

1. If the products themselves get updated, the cart will be updated as well
2. When we add the same item, only the quantity gets updated
3. No need to write hard logic to keep all those states synchronized

> **Note**: You can view the full project implementation [here](https://stackblitz.com/edit/angular-ivy-xajmdv?file=src%2Fapp%2Fapp.component.ts).

## Conclusion

`@ngrx/entity` is a powerful tool, and can be used in a multitude of scenarios. Combined with other tools we have seen in this article (router state, for instance), it can create an implementation of a full UX experience with a minimal amount of code. Note that `@ngrx/entity` has several other functions and nuances not covered here - feel free to explore the [documentation](https://ngrx.io/guide/entity/adapter) and the source code to learn more.