Skip to main content

Chapter 7: Basic Selectors

Using data from Store in components#

So far, we have created Actions that will notify about State changes and a Reducer function that does the actual State modifications. But we have not used the State of our application to display any visual UI corresponding to that State. It is high time we do that. To do that, NgRx uses a concept called Selectors. Let's understand the basics of that.

What are Selectors?#

In most simple terms, Selectors are pure functions that take the State of ur application, and return a piece of it so that it can be used in a component (you will see NgRx extensively uses pure functions). In more detail, in the future chapters, our State will evolve to contain lots of data in the main object, for example, we will have categories, expenses, incomes, probably data about the current user (authentication, full name, etc) and so on. Obviously in a single component we don't need the entire State, thus we write Selectors which will return pieces of that State, for example, a Selector that return only the array of categories to use in the corresponding component.

Those pieces are called "slices" of the State sometimes, but often the term derived State is used, to indicate it is some modified form of the original State. Selectors can also be used to return not just a slice, but some complex calculation of the State, for example, we can write a Selector that calculates the total income based on all the items in the incomes array, and based on that Selector, another one that calculates the average.

This chapter is called "Basic Selectors", so in this one we are going to write only such Selectors that return just slices of the State; in the future chapters, we will write more complex Selectors and learn how to create new Selectors from existing ones, and also how to combine them into new ones.

Building some UI#

Before we proceed, let's first build some UI to show the list of the Categories. In this project, I am using Angular Material to build some pretty interface, but you can use any UI library of your choice (or even not use any). But I suggest you do use Angular Material (we are not going to use too complex components from there, even if you are unfamiliar with it, it won;t be too challenging) to follow the examples easier. You can also visit the Angular Material official documentation to grasp some new knowledge. If you choose to use it, follow this chapter; if not, skip to the next one.

Let's add Angular Material to our project first. Run the following command:

ng add @angular/material

It might ask some multiple choice questions. You may choose a theme, whichever you prefer.

Angular Material has been added to our project. Now, let's add a component that will display the list of categories of expenses/incomes from the Store. I will be using the Container-Presenter approach to build this component. It means we will create two components, one that handles the logic (selects data from the store, in our case), and passes it two the next one, and the other one, which only receives the data as @Input properties and displays the UI. You can read mor e about Container-Presenter here.

In the src/app folder, create a new folder named category-list. Inside that folder, run the following commands:

ng generate component CategoryListContainerng generate component CategoryListPresenter

Now we have two components. Let's start with the presenter, because it is the simple one. Basically, we just need to receive the array of categories as an @Input property, and display the categories using *ngFor. Let's write that code:

// src/app/category-list/category-list-presenter/category-list-presenter.component.tsimport { Component, Input, ChangeDetection } from "@angular/core";
// import the Category interface from wherever you put it
@Component({  selector: "app-category-list-presenter",  templateUrl: "./category-list-presenter.component.html",  changeDetectionStrategy: ChangeDetection.OnPush, // we can use OnPush as we only rely on Input properties for data})export class CategoryListPresenter {  @Input() categories: Category[] = [];}

The component class code is ready - simple as that. Now, to display the list of categories, let's import some components from Angular Material into our AppModule. We will be using the <mat-list> component to display a very basic list of our categories:

// src/app/app.module.ts// other import statements omitted for the sake of brevityimport { MatListModule } from "@angular/material";
@NgModule({  // other metadata omitted  imports: [    // other imports omitted    MatListModule,  ],  declarations: [AppComponent, CategoryListContainer, CategoryListPresenter],})export class AppModule {}

We are all set to write the template for the presenter component:

<--src/app/category-list/category-list-presenter/category-list-presenter.component.html--><mat-list>  <mat-list-item *ngFor="let category of categories"    >{{ category.name }}</mat-list-item  >  <.mat-list></mat-list>

Now the only thing left is to select the data from the Store in the container component, and pass it as @Input to the presenter. To select the data, we need to inject the Store into our component. The Store is a special service-class from NgRx that allows us to interact with the State. To inject that store, we need to import the StoreModule into our AppModule, and provide the Reducer from our previous chapter, so that NgRx knows what is our data an how it is modified. Let;s do this:

// src/app/app.module.ts// other import statements omitted for the sake of brevityimport { StoreModule } from "@ngrx/store";import { reducer } from "./state/reducer";
@NgModule({  // other metadata omitted  imports: [    // other imports omitted    StoreModule.forRoot({ categories: reducer }),  ],})export class AppModule {}

A question arises, why did we provide an object with the reducer function, and not just the reducer? That is because we can have multiple Reducers (and we will in the future chapters), and we can also lazy-load State (we will learn about it in the later chapters too), so we need to provide a mapping of features to reducers. In our case, we only have one feature, that is the categories State, so this is th object. In the future, when we add more feature States, we will rename the Reducers to more accurately reflect the situation. For now, our global state looks like this: {categories: AppState}. Now it is time to write the Selectors.

Writing Selectors#

Selectors are functions, as you remember from a past paragraph. We usually put them in separate files. In the state folder, create a new file named selectors.ts, and let's write our first selector, that return the list of the categories:

// src/app/state/selectors.tsimport { AppState } from "./state";
export const categories = (state: { categories: AppState }) =>  state.categories.categories;

Our state contains a property named categories that is the categories State, which for now is named AppState, as it is all that we have in out app. That's why we have to return state.categories.categories.

And that's it. As promised, the selector is as basic as it can get, it takes the AppState, which, as you remember, looks like this:

export interface AppState {  categories: Category[];}

and returns that categories array.

Now let's use it in the container component. As mentioned, we inject the Store and select the state slice using our brand new selector. Here is how it will look like:

// src/app/category-list/category-list-container/category-list-container.component.tsimport { Component } from "@angular/core";import { Store } from "@ngrx/store";
import { categories } from "../../state/selectors";
@Component({  selector: "app-category-list-presenter",  template:    '<app-category-list-presenter [categories]="categories$ | async"></app-category-list-presenter>',})export class CategoryListContainer {  categories$ = this.store.select(categories);
  constructor(private readonly store: Store) {}}

And that's it. Let's understand what happened here. First, we imported and injected the Store, as promised. Then, we used the store instance to get the data that we wanted. The select method takes a selector and returns an Observable of the slice of the State, in out case, the categories array. It is important that it returns an Observable of that slice, rather than the data itself, this way whenever any part of our application updates the state (for example, by adding a new Category in the Store), we will be instantly notified about it, thus making NgRx magic happen. Then we just pass that unwrapped data to the child component using the async pipe

Note: we wil be using the async pipe extensively, as NgRx works only with Observables.

Now let's create some routing. Let's create a routing module and a route that takes us to the category list page. Create an app.routing.module.ts file and put the following code in it:

// src/app/app.routing.module.ts
import { NgModule } from "@angular/core";import { RouterModule, Routes } from "@angular/router";
import { CategoryListContainerComponent } from "./category-list/category-list-container/category-list-container";
const routes = [  { path: "categories", component: CategoryListContainerComponent },];
@NgModule({  imports: [RouterModule.forRoot(routes)],  exports: [RouterModule],})export class AppRoutingModule {}

Don't forget to add the AppRoutingModule to the imports array in AppModule and put a <router-outlet> in your AppComponent template!

Now open your app and navigate to /categories. Aaaand... nothing. You won't see anything. That is because we don't have any categories in our initialState! It was an empty array, remember? Let's change it a bit, so that it contains the most basic expense everyone makes: Food:

// src/app/state/state.ts
// rest of the file omitted for the sake of brevity
const initialState: AppState = {  categories: [{ name: "Food" }],};

Now you will see the list of the categories when you open the app (basically just Food, but that's enough for now). NgRx magic works!

This chapter has been long, and we set up lots of stuff for future development, and also we have a very basic State at this point, so there won't be any homework to write new Selectors. Instead, let's Create a new State interface that contains the list of categories, and in AppState include it under the name categories, and rename the reducer to categoryReducer. Then, rename the categories array to list. Our selector will then look like this: const categories = (state: AppState) => state.categories.list.

We will be writing new Selectors a lot in the future though, so be prepared.

We learned how to create Selectors and use them to display data from the Store in our UI. Next, we will learn how to trigger State changes and see the actual benefit of NgRx.

Exercise solution
state.ts
export interface CategoryState {  list: Category[];}
export interface AppState {  categories: CategoryState;}
app.module.ts
@NgModule({  // other metadata  imports: [    // other imports    StoreModule.forRoot({ categories: categoriesReducer }),  ],})export class AppModule {}
selectors.ts
const categories = (state: AppState) => state.categories.list;