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