NGXS

The power of selectors

NGXS Core Team

NGBE Core Team

Angular Architect at Riaktr

Coach at HackYourFuture

Mateus Carniatto

Writer for inDepth.dev

Why NGXS?

  • Levrage TS decorators (less boilerplate)

  • Dependency Injection

  • Progressive (Plugins and NGXS-labs)

  • Has a great community

Key concepts

import { Injectable } from '@angular/core';
import { State } from '@ngxs/store';

export interface Todo {
  name: string;
  completed: boolean;
}

@State<Todo[]>({
  name: 'todos',
  defaults: []
})
@Injectable()
export class TodoState {}

State

$ npm i @ngxs/store --save

Module

import { NgModule } from '@angular/core';
import { NgxsModule } from '@ngxs/store';
import environment from 'environment';

@NgModule({
  imports: [
    NgxsModule.forRoot([TodoState], {
      developmentMode: !environment.production
    })
  ]
})
export class AppModule {}
export namespace TodoActions {
  export class Load {
    static readonly type = '[Todo] Load';
  }

  export class Add {
    static readonly type = '[Todo] Add';
    constructor(public todo: Todo) {}
  }
}

Action

import { State, Action, StateContext } from '@ngxs/store';
import { TodoActions } from './state/todo.actions'

@State<Todo[]>({...})
@Injectable()
export class TodoState {

  constructor(private backendService: BackendService) {}
  
  @Action(TodoActions.Add)
  add(ctx: StateContext<Todo[]>, { name }: TodoActions.Add) {
    const newTodo = {name, completed: false};
    ctx.setState([...ctx.getState, newTodo])
  }
  
  @Action(TodoActions.Load)
  load(ctx: StateContext<Todo[]>) {
    return this.backendService.loadTodos().pipe(
      tap( (todos: Todo[]) => ctx.setState(todos))
    );
  }
  
}

Action Handlers

Dispatching Actions

import { TodoActions } from './state/todo.actions'

@Component({ ... })
export class TodoComponent implements OnInit{

  constructor(private store: Store) {}

  ngOnInit() {
    this.store.dispatch(new TodoActions.Load());
  }

  todoAdded(todo: Todo) {
    this.store.dispatch(new TodoActions.Add(todo))
  }
}

Select

import { Select, Store } from "@ngxs/store";

@Component({ ... })
export class TodoComponent implements OnInit{
  @Select(state => state.todos) todos$: Observable<Todo[]>;

  constructor(private store: Store) {}

  ngOnInit() {
    
    const completed$ = this.store.select(state => state.todos).pipe(
      map(todos => todos.filter(todo => todo.completed))
    );
    
  }
}

Selectors

import { State, Selector, Select } from '@ngxs/store'

@State<Todo[]>({...})
@Injectable()
export class TodoState {

  @Selector()
  static getCompleted(state: Todo[]) {
    return state.filter(todo => todo.completed);
  }
  
}

@Component{...}
export class TodoComponent {
  @Select(TodoState.getCompleted) completed$: Observable<Todo[]>;
  
  constructor(private store: Store) {
    this.completed$ = this.store.select(TodoState.getCompleted);
  }
}

State Operators

  • insertItem
  • removeItem
  • updateItem
  • append
  • patch
  • compose

State Operators

import { TodoActions } from './state/todo.actions'
import { updateItem  } from '@ngxs/store/operators'

@State<Todo[]>({...})
@Injectable()
export class TodoState {
  /* ... */
  
  @Action(TodoActions.CompleteTodo)
  completeTodo(ctx: StateContext<Todo[]>, { id }: TodoActions.CompleteTodo) {
    ctx.setState(updateItem(id, patch({completed: true})))
  }
  
}

State Operators

import { TodoActions } from './state/todo.actions'
import { insertItem, removeItem, updateItem  } from '@ngxs/store/operators'

@State<Todo[]>({...})
@Injectable()
export class TodoState {

  constructor(private backendService: BackendService) {}
  
  @Action(TodoActions.AddTodo)
  completeTodo(ctx: StateContext<Todo[]>, { todo }: TodoActions.AddTodo) {
    ctx.setState(insertItem(todo))
  }
  
  @Action(TodoActions.RemoveTodo)
  completeTodo(ctx: StateContext<Todo[]>, { id }: TodoActions.RemoveTodo) {
    ctx.setState(removeItem(id))
  }
  
  @Action(TodoActions.CompleteTodo)
  completeTodo(ctx: StateContext<Todo[]>, { id }: TodoActions.CompleteTodo) {
    ctx.setState(updateItem(id, todo => ({...todo, completed: true})))
  }
}

Demo

export interface RecipeStateModel {
  recipes: Recipe[];
  selectedRecipes: number[];
}

export interface Recipe {
  id: number;
  name: string;
  type: RecipeType;
  ingredients: Ingredient[];
}

export interface Ingredient {
  name: string;
  quantity: number;
  unit: string;
}

export type RecipeType = 'breakfast' | 'lunch' | 'dinner';

Model

Selectors

@State<RecipeStateModel>({ 
  name: "recipes",
  defaults: {
    recipes: [],
    selectedRecipes: [],
  }
})
@Injectable()
export class RecipeState {
  
  @Selector()
  static recipes(state: RecipeStateModel) {
    return state.recipes;
  }
  
  @Selector()
  static selectedRecipes(state: RecipeStateModel) {
    return state.selectedRecipes;
  }
  
}

Selector Options

import { NgModule } from '@angular/core';
import { NgxsModule } from '@ngxs/store';

@NgModule({
  imports: [
    NgxsModule.forRoot([TodoState], {
      developmentMode: !environment.production,
      selectorOptions: {
        suppressErrors: false,
        injectContainerState: false
      },
    }),
  ]
})
export class AppModule {}

Update Selectors

@State<RecipeStateModel>({ 
  name: "recipes",
  defaults: {
    recipes: [],
    selectedRecipes: [],
  }
})
@Injectable()
export class RecipeState {
  
  @Selector([RecipeState])
  static recipes(state: RecipeStateModel) {
    return state.recipes;
  }
  
  @Selector([RecipeState])
  static selectedRecipes(state: RecipeStateModel) {
    return state.selectedRecipes;
  }
  
}

Extract Selectors

export class StateSelectors {
  
  @Selector([RecipeState])
  static recipes(state: RecipeStateModel) {
    return state.recipes;
  }
  
  @Selector([RecipeState])
  static selectedRecipes(state: RecipeStateModel) {
    return state.selectedRecipes;
  }
  
}

@Component({...})
export class AppComponent {
  @Select(StateSelectors.recipes) recipes$: Observable<Recipe[]>;             
}

take aways

Adjust your selector options:

  • enable selector errors
  • control your dependencies

 

Extract your selectors to their own class:

  • easier to test
  • separation of concerns

Selection count

Initial approach

export class StateSelectors {
  
  @Selector([RecipeState])
  static recipes(state: RecipeStateModel) {
    return state.recipes;
  }
  
  @Selector([RecipeState])
  static selectedRecipes(state: RecipeStateModel) {
    return state.selectedRecipes;
  }
  
  @Selector([RecipeState])
  static selectedCount(state: RecipeStateModel) {
    return state.selectedRecipes.length;
  }
  
}

@Component({...})
export class AppComponent {
  @Select(StateSelectors.selectedCount) selectedCount$: Observable<number>;             
}

Composing selectors

export class StateSelectors {
  
  @Selector([RecipeState])
  static recipes(state: RecipeStateModel) {
    return state.recipes;
  }
  
  @Selector([RecipeState])
  static selectedRecipes(state: RecipeStateModel) {
    return state.selectedRecipes;
  }
  
  @Selector([StateSelectors.selectedRecipes])
  static selectedCount(selectedRecipes: Recipe[]) {
    return selectedRecipes.length;
  }
  
}

@Component({...})
export class AppComponent {
  @Select(StateSelectors.selectedCount) selectedCount$: Observable<number>;             
}

take aways

Compose selectors:

  • recalculate only when needed
  • reduces code repetition

 

Shopping List

Shopping List

0
 Advanced issue found
export function retrieveShoppingList(recipes, selectedRecipes) {
  const count = _countBy(selectedRecipes)
  return Object.keys(count)
    .map( id => {
    const recipe = recipes.find(rcp => rcp.id === +id)
    return recipe.ingredients.map( ingredient => ({
      ...ingredient,
      quantity: ingredient.quantity * count[id]
    } as Ingredient)
                                 )
  }).reduce((acc, curr) => {
    return acc = [...acc, ...curr];
  }, []);
}

Initial approach

@Component({...})
export class AppComponent implements OnInit {
  @Select(StateSelectors.recipes) recipes$: Observable<Recipe[]>;

  @Select(StateSelectors.selectedRecipes)
  selectedRecipes$: Observable<number[]>;
  
  shoppingList$: Observable<Ingredient[]>;
  
  ngOnInit() {
    this.shoppingList$ = combineLatest([
      recipes$,
      selectedRecipes$
    ]).pipe(
      map(([recipes, selectedRecipes]) => {
        retrieveShoppingList(recipes, selectedRecipes))
      }
    )
  }
}

Moving to selectors

export class StateSelectors {
  
 /* previous selectors ... */
  
  @Selector([StateSelectors.recipes, StateSelectors.selectedRecipes])
  static shoppingList(recipes: Recipe[], selectedRecipes: number[]) {
    return retrieveShoppingtList(recipes, selectedRecipes)
  }
  
}

take aways

Move your business logic to selectors:

  • keep your presentational components dumb
  • improves maintainability
  • reduces the need of manipulating streams

Filter by type

Initial approach

export class StateSelectors {
  
  @Selector([RecipeState])
  static recipes(state: RecipeStateModel) {
    return state.recipes;
  }
  
  @Selector([RecipeState])
  static filter(state: RecipeStateModel) {
    return state.filter;
  }
  
  @Selector([RecipeState])
  static selectedRecipes(state: RecipeStateModel) {
    return state.selectedRecipes;
  }
  
  @Selector([StateSelectors.recipes, StateSelectors.filter])
  static recipesByType(recipes: Recipe[], filter: RecipeType) {
    return !filter 
      ? recipes 
      : recipes.filter(recipe => recipe.type === filter);
  }
  
}

Using lazy selector

export class StateSelectors {
  
  @Selector([RecipeState])
  static recipes(state: RecipeStateModel) {
    return state.recipes;
  }
  
  @Selector([RecipeState])
  static selectedRecipes(state: RecipeStateModel) {
    return state.selectedRecipes;
  }
  
  @Selector([StateSelectors.recipes])
  static getRecipesByTypeFn(recipes: Recipe[]) {
    return (filter: RecipeType) => !filter 
      ? recipes 
      : recipes.filter(recipe => recipe.type === filter);
  }
  
}

Using lazy selector

@Component({...})
export class AppComponent {
  @Select(StateSelectors.getRecipesByTypeFn) 
  getRecipesByTypeFn: Observable<(filter: RecipeType) => Recipe[]>;             
  
  filter: RecipeType;

  selectFilter(filter: RecipeType) {
    this.filter = filter;
  }
}

Using lazy selector

  <div class="tabs">
    <ul>
      <li [class.is-active]="!filter" 
          (click)="selectFilter(null)">
        <a>All</a>
      </li>
      <li [class.is-active]="filter === 'breakfast'"
          (click)="selectFilter('breakfast')">
        <a>Breakfast</a>
      </li>
      <li [class.is-active]="filter === 'lunch'"
          (click)="selectFilter('lunch')">
        <a>Lunch</a>
      </li>
      <li [class.is-active]="filter === 'dinner'"
          (click)="selectFilter('dinner')">
        <a>Dinner</a>
      </li>
    </ul>
  </div>

  <ng-container *ngIf="recipeByTypeFn$ | async as recipeByTypeFn">
    <div class="recipe" *ngFor="let recipe of recipeByTypeFn(filter)">
      {{recipe.name}}
      <button class="button" (click)="selectRecipe(recipe.id)"> + </button>
    </div>  
  </ng-container>

take aways

Leverage lazy selectors:

  • only run the code when needed
  • avoid running the same code twice (memoization)
  • keep your state lean

Bonus tip

import { NgModule } from '@angular/core';
import { NgxsModule } from '@ngxs/store';

@NgModule({
  imports: [
    NgxsModule.forRoot([TodoState], {...}),
    
    // Enables Redux devTools
    NgxsReduxDevtoolsPluginModule.forRoot({
      name: 'Recipes App',
      disabled: environment.production
    })
    
  ]
})
export class AppModule {}
$ npm install @ngxs/devtools-plugin --save-dev

Main takeaways

  • Use TS namespace for actions
  • Use state operators
  • Adjust your selector options
  • Extract selectors to their on class
  • Compose selectors
  • Move your business logic to selectors
  • Leverage lazy selectors

@c4rniatto

Thank You

ngxs.io

NGXS: Power of Selectors

By Mateus Carniatto