DEV Community

loading...
Cover image for Maximizing and Simplifying Component Views with NgRx Selectors

Maximizing and Simplifying Component Views with NgRx Selectors

brandontroberts profile image Brandon Roberts ・7 min read

Originally published on brandonroberts.dev.

When building Angular 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:

interface Product {
  id: string;
  name: string;
  description: string;
}
Enter fullscreen mode Exit fullscreen mode

A example products state interface looks like this:

interface ProductsState {
  collection: Product[];
  loaded: boolean;
}
Enter fullscreen mode Exit fullscreen mode

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

Selecting the total number of products in the collection.

const selectTotalProducts = createSelector(
  selectProductsState,
  state => state.collection.length
);
Enter fullscreen mode Exit fullscreen mode

Select the first 5 products from the collection:

const selectFirstFiveProducts = createSelector(
  selectProductsState,
  state => state.collection.slice(0, 5)
);
Enter fullscreen mode Exit fullscreen mode

Create a dictionary of products by id:

const selectProductsDictionary = createSelector(
  selectAllProducts,
  products => {
    let productsDictionary: { [id: number]: Product } = {};

    products.forEach(product => {
      productsDictionary[product.id] = product;
    });

  return productsDictionary;
});
Enter fullscreen mode Exit fullscreen mode

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 is used in many different ways, and should be extracted into its own selector.

const selectAllProducts = createSelector(
  selectProductsState,
  state => state.collection
);
Enter fullscreen mode Exit fullscreen mode

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

const selectTotalProducts = createSelector(
  selectAllProducts,
  products => products.length
);
Enter fullscreen mode Exit fullscreen mode

Along with selecting the first 5 products from the collection.

const selectFirstFiveProducts = createSelector(
  selectAllProducts,
  products => products.slice(0, 5)
);
Enter fullscreen mode Exit fullscreen mode

A benefit you gain by using selectors to build other selectors is that selectors only recompute when their 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's inputs do change, but its 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 over at inDepthDev.

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

interface Product {
  id: string;
  name: string;
  description: string;
  categoryId: string;
}
Enter fullscreen mode Exit fullscreen mode

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

interface Category {
  id: string;
  name: string;
  description: string
}
Enter fullscreen mode Exit fullscreen mode

A example categories state interface looks like this:

interface CategoriesState {
  collection: Category[];
  loaded: boolean;
}
Enter fullscreen mode Exit fullscreen mode

Apply the same approach to selecting all categories.

const selectAllCategories = createSelector(
  selectCategoriesState,
  state => state.collection
);
Enter fullscreen mode Exit fullscreen mode

Create a dictionary of categories by id:

const selectCategoriesDictionary = createSelector(
  selectAllCategories,
  categories => {
    let categoriesDictionary: { [id: number]: Category } = {};

    categories.forEach(category => {
      categoriesDictionary[category.id] = category;
    });

  return categoriesDictionary;
});
Enter fullscreen mode Exit fullscreen mode

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

const selectProductsList = createSelector(
  selectAllProducts,
  selectCategoriesDictionary,
  (products, categoriesDictionary) => {
    return products.map(product => {
      return {
        ...product,
        title: `${product.name} details`,
        category: categoriesDictionary[product.categoryId] ? categoriesDictionary[product.categoryId].name : '';
      };
    });
Enter fullscreen mode Exit fullscreen mode

NgRx Tip: The @ngrx/entity package creates dictionaries of collections for you, and provides an adapter with methods, and selectors for working with collections out of the box.

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:

const selectProductsLoaded = createSelector(
  selectProductsState,
  state => state.loaded
);
Enter fullscreen mode Exit fullscreen mode

And select if the categories are loaded:

const selectCategoriesLoaded = createSelector(
  selectCategoriesState,
  state => state.loaded
);
Enter fullscreen mode Exit fullscreen mode

Create an "is ready" selector to combine the other two selectors.

const selectIsViewReady = createSelector(
  selectProductsLoaded,
  selectCategoriesLoaded,
  (productsLoaded, categoriesLoaded) => [productsLoaded, categoriesLoaded].every(loaded => loaded === true);
Enter fullscreen mode Exit fullscreen mode

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.

A common pattern is to combine multiple observables using the combineLatest operator from RxJS in the component class.

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 {
  ready$ = this.store.select(ProductListSelectors.selectIsViewReady);
  products$ = this.store.select(ProductListSelectors.selectProductsList);
  vm$ = combineLatest([this.ready, this.products$]).pipe(
    map(([ready, products]) => ({ ready, products }))
  );

  constructor(private store: Store) {}

  ngOnInit() {
    this.store.dispatch(ProductsListActions.enter());
  }
}
Enter fullscreen mode Exit fullscreen mode

Combining observables in RxJS using combineLatest or other combination operators have their place, but both observables are getting data from the same global state object. And the combined observable will emit a value any time either one of the observables emits a value after the first combined emission. This causes extra computations that aren't necessary when multiple pieces of state you are combining are updated at the same time. The more observables added to the combineLatest array results in more computations when multiple pieces of state are updated.

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.

export const selectProductListViewModel = createSelector(
  selectIsViewReady,
  selectProductsList,
  (ready, products) => ({ ready, products })
);
Enter fullscreen mode Exit fullscreen mode

A combined selector gives you fewer observables to manage, a single emission even when multiple slices of state used in the selector are updated in one state change event, and a clean view model to use in your component. Now instead of having multiple observables for ready status and the product list, there is a single view model observable.

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());
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

<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>
Enter fullscreen mode Exit fullscreen mode

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. 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.

Follow me on Twitter, YouTube, Twitch.

Discussion (1)

pic
Editor guide
Collapse
paulmojicatech profile image
paulmojicatech

This is very helpful on how to combine selectors and something I will implement at work. Thanks Brandon!