NGXS Core Team
NGBE Core Team
Angular Architect at Riaktr
Coach at HackYourFuture
Mateus Carniatto
Writer for inDepth.dev
Levrage TS decorators (less boilerplate)
Dependency Injection
Progressive (Plugins and NGXS-labs)
Has a great community
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 {}
$ npm i @ngxs/store --save
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) {}
}
}
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))
);
}
}
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))
}
}
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))
);
}
}
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);
}
}
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})))
}
}
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})))
}
}
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';
@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;
}
}
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 {}
@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;
}
}
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[]>;
}
Adjust your selector options:
Extract your selectors to their own class:
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>;
}
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>;
}
Compose selectors:
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];
}, []);
}
@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))
}
)
}
}
export class StateSelectors {
/* previous selectors ... */
@Selector([StateSelectors.recipes, StateSelectors.selectedRecipes])
static shoppingList(recipes: Recipe[], selectedRecipes: number[]) {
return retrieveShoppingtList(recipes, selectedRecipes)
}
}
Move your business logic to selectors:
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);
}
}
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);
}
}
@Component({...})
export class AppComponent {
@Select(StateSelectors.getRecipesByTypeFn)
getRecipesByTypeFn: Observable<(filter: RecipeType) => Recipe[]>;
filter: RecipeType;
selectFilter(filter: RecipeType) {
this.filter = filter;
}
}
<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>
Leverage lazy selectors:
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
@c4rniatto
ngxs.io