Skip to content

Commit

Permalink
feat: add post on maximizing and simplifying component views with sel…
Browse files Browse the repository at this point in the history
…ectors
  • Loading branch information
brandonroberts committed Dec 11, 2020
1 parent 0c2f516 commit ce3dc87
Show file tree
Hide file tree
Showing 5 changed files with 278 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
---
title: 'Maximizing and Simplifying Component Views with NgRx Selectors'
description: 'Deriving data, composing selectors, and building view models efficiently with NgRx Selectors'
published: true
slug: 2020-12-14-maximizing-simplifying-component-views-ngrx-selectors
publishedDate: '2020-12-14 02:00 PM CST'
---

<br/>

<a href="hhttps://unsplash.com/@siora18?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText" title="Siora Photography on Unsplash">
<img src="/assets/posts/siora-photography-L06-OsgvNoM-unsplash.jpg" width="100%"/>
</a>

When building applications with NgRx for state management, one area that provides a lot of power and flexibility is in the usage of selectors. At a high level, selectors provide a few benefits to querying data in the store: efficient querying of data through memoization, composability to build up new data models, and synchronous access to operate with state. When reviewing projects and their usage of NgRx along with selectors, there are a few common trends that stick out including under-utilizing selectors for combining data, storing data that can be derived, and minimal usage of composed selectors to build view models. This post provides some practical examples in these areas to show how you can maximize and simplify your components using selectors by deriving state, combining and composing selectors together, and building view models from selectors for your components.

## Deriving State

There are many ways to slice up the data in the store to get the data you need for different views in your application. Derived data is a new combination of data produced from data that already exists in the store. Using a list of products as an example, let’s look at how you can use selectors to return different perspectives from the same dataset.

Let’s start with an interface for a product:

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

A example products state interface looks like this:

```ts
interface ProductsState {
collection: Product[];
loaded: boolean;
}
```

So what are some things we can derive, and ways we can transform this data? Here are a few examples.

Selecting the total number of products in the collection.

```ts
const selectAll = createSelector(
selectProductsState,
state => state.collection.length
);
```

Select the top 5 products from the collection:

```ts
const topFiveProducts = createSelector(
selectProductsState,
state => state.collection.slice(0, 4)
);
```

Create a dictionary of products by id:

```ts
const productsDictionary = createSelector(
selectProductsState,
state => {
return state.collection
.reduce((productDictionary, product) => {
return {
...productDictionary,
[product.id]: product
};
}, {});
});
```

These are just a few ways of deriving new data from the existing state, but you have many options depending on datasets you need to build.

## Composing Selectors

In the previous examples, selectors were built by accessing each property on the state, and returning a different set of data. Selectors are composable, in that you use selectors to build using other selectors, providing them as inputs. These input selectors can come from many different areas, even ones outside your immediate state. Taking the products example from above, the products collection in many different ways, and should be extracted into its own selector.

```ts
const selectAllProducts = createSelector(
selectProductsState,
state => state.collection
);
```

Now the total products selector use this selector as an input.

```ts
const selectTotalProducts = createSelector(
selectAllProducts,
products => products.length
);
```

Along with selecting the top 5 products from the collection.

```ts
const topFiveProducts = createSelector(
selectAllProducts,
products => products.slice(0, 4)
);
```

A big benefit you gain by using selectors to build other selectors is that selectors only recompute when they’re inputs change. By only listening to the collection instead of the entire state, the composed selectors will only re-run the projector function if the collection changes. The other benefit is that if a selector inputs do change, but it’s computed value is the same, the previous value is returned, along with the same reference. This is where you get the added efficiency when using OnPush change detection. If the reference hasn’t changed, change detection doesn’t need to run again. To learn more about the ins and outs of change detection, read [Everything you need to know about change detection in Angular](https://indepth.dev/posts/1053/everything-you-need-to-know-about-change-detection-in-angular) over at [inDepthDev](https://indepth.dev).

To drive the composability of selectors even further, modify the products state to add a categoryId to each product.

```ts
interface Product {
id: string;
name: string;
description: string;
categoryId: string;
}
```

Along with products, add a slice of state to manage categories. The model for a category is similar to a product.

```ts
interface Category {
id: string;
name: string;
description: string
}
```

A example categories state interface looks like this:

```ts
interface CategoriesState {
collection: Category[];
loaded: boolean;
}
```

Apply the same approach to selecting all categories.

```ts
const selectAllCategories = createSelector(
selectCategoriesState,
state => state.collection
);
```

Build on the same idea that selectors are composable to build a new dataset of products along with their associated category and title.

```ts
const selectProductsList = createSelector(
selectAllProducts,
selectAllCategories,
(products, categories) => {
return products.map(product => {
return {
...product,
title: `${product.name} details`,
category: categories.find(category => category.id === product.categoryId) || ‘’;
};
});
```
<video width="100%" height="480" loop autoplay controls>
<source src="/assets/posts/another-one.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
To create a new selector composed of the loaded properties of two states into one, use them as inputs to another selector.
Select if the collection is loaded:
```ts
const selectProductsLoaded = createSelector(
selectProductsState,
state => state.loaded
);
```
And select if the categories are loaded:
```ts
const selectCategoriesLoaded = createSelector(
selectCategoriesState,
state => state.loaded
);
```
Create an "is ready" selector to combine the other two selectors.
```ts
const selectIsViewReady = createSelector(
selectProductsLoaded,
selectCategoriesLoaded,
(productsLoaded, categoriesLoaded) => [productsLoaded, categoriesLoaded].every(loaded => loaded === true);
```
In this example, when the returned value is updated whenever either of the loaded properties is updated, producing a single value of all the loaded states. All the state is already in the store, so the data can be combined before consuming it as an observable.
## Building View Models
When you are consuming many observables in your components, a good pattern to follow is to build a view model of the combined observables into one single observable that’s exposed to your template. This view model pattern is very popular in AngularJS, and Angular. In Angular, you only have to deal with unwrapping a single observable with the async pipe, and you’re able to work with the view model properties throughout the rest of your template. Building on top of composable selectors, you can achieve this same pattern, and keep the same efficiency in selecting data from the Store.
In the previous selectors, there is a value for when the view is ready, and the list of products. Use these two selectors to build a view model selector for the product list component.
```ts
export const selectProductListViewModel = createSelector(
selectIsViewReady,
selectProductsList,
(ready, products) => ({ ready, products })
);
```
Now instead of having two different observables for ready status and the product list, there is a single view model observable.
```ts
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';

import * as ProductListSelectors from './product-list.selectors';
import * as ProductsListActions from './product-list.actions';

@Component({
selector: 'app-product-list',
templateUrl: './product-list.component.html',
styleUrls: ['./product-list.component.css']
})
export class ProductListComponent {
vm$ = this.store.select(ProductListSelectors.selectProductListViewModel);

constructor(private store: Store) {}

ngOnInit() {
this.store.dispatch(ProductsListActions.enter());
}
}
```
In the template, use the async pipe to subcribe to and assign the variable to the `vm` property, and access the properties on the view model in the template.
```html
<h2>Products</h2>

<ng-container *ngIf="vm$ | async as vm">
<ng-container *ngIf="vm.ready;else loading">
<div *ngFor="let product of vm.products">

<h3>
<a [title]="product.title" [routerLink]="['/products', product.id]">
{{ product.name }}
</a>
</h3>
<p *ngIf="product.description">
Description: {{ product.description }}
</p>

<p *ngIf="product.category">
Category: {{ product.category }}
</p>
</div>
</ng-container>
</ng-container>

<ng-template #loading>
Loading ...
</ng-template>
```
In case you have more data for a view model, selectors can take up to 8 inputs to combine data. If you exceed that limit, break down your selectors into smaller units, and compose them back together into a single one.
NOTE: In the examples above, the collections are managed in the state by using simple arrays. In practice, the @ngrx/entity library would be used to manage these collections consistently, efficiently, and predictably using an adapter with provided selectors.
The component remains thin, and takes full advantage of observables through selectors provided through the Store. You can maximize and simplify component views with NgRx Selectors by deriving new data from existing data, composing selectors together, and building reactive view models for your component to consume.
To see a full working example that builds on top of the Angular Getting Started tutorial, check out this [repository](https://github.com/brandonroberts/maximize-simplify-ngrx-selectors).
Follow me on [Twitter](https://twitter.com/brandontroberts), [YouTube](https://youtube.com/brandonrobertsdev), [Twitch](https://twitch.tv/brandontroberts).
8 changes: 0 additions & 8 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,6 @@ import { map } from 'rxjs/operators';
<a href="https://github.com/brandonroberts" title="GitHub">
<img src="assets/images/logos/github-icon.svg" />
</a>
<iframe
src="https://github.com/sponsors/brandonroberts/button"
title="Sponsor brandonroberts"
height="35"
width="107"
style="border: 0;margin-left: 16px;"
></iframe>
</div>
</mat-toolbar>
Expand Down
Binary file added src/assets/posts/another-one.mp4
Binary file not shown.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit ce3dc87

Please sign in to comment.