State Management In Angular With NGXS

State Management, in layman's terms, refers to a way we can store data, modify it, and react to its changes and make it available to the UI components.
 
Recently, I got a chance to work on state management in my angular application. I used NGXS library for the same.
 
Just a brief note on NGXS, it is a state management pattern + library for Angular.  
 
It acts as a single source of truth for your application's state, providing simple rules for predictable state mutations. NGXS is modeled after the CQRS pattern popularly implemented in libraries like Redux and NgRx but reduces boilerplate by using modern TypeScript features such as classes and decorators.
 
There are 4 major concepts to NGXS,
  • Store: Global state container, action dispatcher and selector
  • Actions: Class describing the action to take and its associated metadata
  • State: Class definition of the state
  • Selects: State slice selectors
Now, let's quickly start with our real work 
 
FYI: I am working with the following configuration,
  • Angular CLI: 11.2.5
  • Node: 12.18.3
  • OS: win32 x64 

Create new Angular Application

  1. ng new state-management-with-ngxs-app  
Enforce stricter type checking -> No
 
Add angular routing -> Yes
 
Stylesheet format -> css
 

Change directory and open solution in VS Code (you can use any editor that you prefer). 

  1. cd state-management-with-ngxs-app  
  2. code .   

Install NGXS store

 
Now, in cmd or terminal of vs code, use the following command to install NGXS Store
  1. npm install @ngxs/store --save   

Add imports in app.module.ts of an angular application

 
Import NgxsModule from @ngxs/store in app.module.ts 
  1. import { NgModule } from '@angular/core';  
  2. import { BrowserModule } from '@angular/platform-browser';  
  3.   
  4. import { AppRoutingModule } from './app-routing.module';  
  5. import { AppComponent } from './app.component';  
  6.   
  7. import {NgxsModule} from '@ngxs/store';  
  8.   
  9. @NgModule({  
  10.   declarations: [  
  11.     AppComponent  
  12.   ],  
  13.   imports: [  
  14.     BrowserModule,  
  15.     AppRoutingModule,  
  16.     NgxsModule.forRoot()  
  17.   ],  
  18.   providers: [],  
  19.   bootstrap: [AppComponent]  
  20. })  
  21. export class AppModule { }  

Install and import Bootstrap

 
For the sake of making our components look good in minimum efforts, we will make use of bootstrap.
 
Install bootstrap.
  1. npm install bootstrap --save
Import bootstrap by putting the following line in styles.css
  1. @import "~bootstrap/dist/css/bootstrap.min.css"
Also, to have a model-driven approach to handle form inputs, we will use Reactive Forms.
 
Import ReactiveFormsModule in app.module.ts file. So now, your app.module.ts will would look like: 
  1. import { NgModule } from '@angular/core';  
  2. import { BrowserModule } from '@angular/platform-browser';  
  3.   
  4. import { AppRoutingModule } from './app-routing.module';  
  5. import { AppComponent } from './app.component';  
  6.   
  7. import {NgxsModule} from '@ngxs/store';  
  8. import { ReactiveFormsModule } from '@angular/forms';  
  9.   
  10.   
  11. @NgModule({  
  12.   declarations: [  
  13.     AppComponent  
  14.   ],  
  15.   imports: [  
  16.     BrowserModule,  
  17.     AppRoutingModule,  
  18.     NgxsModule.forRoot(),  
  19.     ReactiveFormsModule  
  20.   ],  
  21.   providers: [],  
  22.   bootstrap: [AppComponent]  
  23. })  
  24. export class AppModule { }   

Create components

 
In src -> app, create a new folder called components.
 
In cmd / terminal, navigate to the path of this components folder, and generate 2 new components: user-form and user-list.
  1. ng g c user-form  
  2. ng g c user-list
Open the app.component.html file and add following code 
  1. <div class="container">  
  2.   <div class="row">  
  3.       <div class="col-6">  
  4.           <app-user-form></app-user-form>  
  5.       </div>  
  6.       <div class="col-6">  
  7.           <app-user-list></app-user-list>  
  8.       </div>  
  9.   </div>  
  10. </div>  
Put following code in user-form.component.html 
  1. <div class="card mt-5">  
  2.     <div class="card-body">  
  3.         <h5 class="card-title">Add / Edit user</h5>  
  4.         <form [formGroup]="userForm" (ngSubmit)="onSubmit()">  
  5.             <div class="form-group">  
  6.                 <label>Name</label>  
  7.                 <input type="text" class="form-control" formControlName="name" #name/>  
  8.             </div>  
  9.             <div class="form-group">  
  10.                 <label>City</label>  
  11.                 <input type="text" class="form-control" formControlName="city" #city/>  
  12.             </div>  
  13.             <div class="form-group">  
  14.                 <button type="submit" class="btn btn-success mr-2">  
  15.                     Save  
  16.                 </button>  
  17.                 <button class="btn btn-primary" (click)="clearForm()">  
  18.                     Clear  
  19.                 </button>  
  20.             </div>  
  21.         </form>  
  22.     </div>  
  23. </div>  
Put following code in user-form.component.ts
  1. import { Component, OnInit } from '@angular/core';  
  2. import { User } from 'src/app/models/user';  
  3. import { Observable, Subscription } from 'rxjs';  
  4. import { Select, Store } from '@ngxs/store';  
  5. import { UserState } from 'src/app/states/user.state';  
  6. import { FormGroup, FormBuilder } from '@angular/forms';  
  7. import { UpdateUser, AddUser, SetSelectedUser } from 'src/app/actions/user.action';  
  8.   
  9. @Component({  
  10.   selector: 'app-user-form',  
  11.   templateUrl: './user-form.component.html',  
  12.   styleUrls: ['./user-form.component.css']  
  13. })  
  14. export class UserFormComponent implements OnInit {  
  15.   
  16.   @Select(UserState.getSelectedUser) selectedUser: Observable<User>;  
  17.   
  18.     userForm: FormGroup;  
  19.     editUser = false;  
  20.     private formSubscription: Subscription = new Subscription();  
  21.   
  22.     constructor(private fb: FormBuilder, private store: Store) {  
  23.         this.createForm();  
  24.     }  
  25.   
  26.     ngOnInit() {  
  27.       this.formSubscription.add(  
  28.         this.selectedUser.subscribe(user => {  
  29.           if (user) {  
  30.             this.userForm.patchValue({  
  31.               id: user.id,  
  32.               name: user.name,  
  33.               city: user.city  
  34.             });  
  35.             this.editUser = true;  
  36.           } else {  
  37.             this.editUser = false;  
  38.           }  
  39.         })  
  40.       );  
  41.     }  
  42.   
  43.     createForm() {  
  44.       this.userForm = this.fb.group({  
  45.           id: [''],  
  46.           name: [''],  
  47.           city: ['']  
  48.       });  
  49.     }  
  50.   
  51.     onSubmit() {  
  52.       if (this.editUser) {  
  53.         this.store.dispatch(new UpdateUser(this.userForm.value, this.userForm.value.id));  
  54.         this.clearForm();  
  55.       } else {  
  56.         this.store.dispatch(new AddUser(this.userForm.value));  
  57.         this.clearForm();  
  58.       }  
  59.     }  
  60.   
  61.     clearForm() {  
  62.       this.userForm.reset();  
  63.       this.store.dispatch(new SetSelectedUser(null));  
  64.     }  
  65.   
  66. }   
Add following code in user-list.component.html
  1. <div class="col">  
  2.     <div *ngIf="(users| async)?.length > 0; else noRecords">  
  3.         <table class="table table-striped">  
  4.             <thead>  
  5.             <tr>  
  6.                 <th>Name</th>  
  7.                 <th>City</th>  
  8.                 <th></th>  
  9.                 <th></th>  
  10.             </tr>  
  11.             </thead>  
  12.             <tbody>  
  13.             <tr *ngFor="let user of users | async">  
  14.                 <td>{{ user.name }}</td>  
  15.                 <td>{{ user.city }}</td>  
  16.                 <td>  
  17.                     <button class="btn btn-primary" (click)="editUser(user)">Edit</button>  
  18.                 </td>  
  19.                 <td>  
  20.                     <button class="btn btn-danger" (click)="deleteUser(user.id)">Delete</button>  
  21.                 </td>  
  22.             </tr>  
  23.             </tbody>  
  24.         </table>  
  25.     </div>  
  26.     <ng-template #noRecords>  
  27.         <table class="table table-striped mt-5">  
  28.             <tbody>  
  29.             <tr>  
  30.                 <td><p>No users to show!</p></td>  
  31.             </tr>  
  32.             </tbody>  
  33.         </table>  
  34.     </ng-template>  
  35. </div>  
Put following code in user-list.component.ts 
  1. import { Component, OnInit } from '@angular/core';  
  2. import { User } from 'src/app/models/user';  
  3. import { Observable } from 'rxjs';  
  4. import { Select, Store } from '@ngxs/store';  
  5. import { UserState } from 'src/app/states/user.state';  
  6. import { DeleteUser, SetSelectedUser } from 'src/app/actions/user.action';  
  7.   
  8. @Component({  
  9.   selector: 'app-user-list',  
  10.   templateUrl: './user-list.component.html',  
  11.   styleUrls: ['./user-list.component.css']  
  12. })  
  13. export class UserListComponent implements OnInit {  
  14.   
  15.   @Select(UserState.getUserList) users: Observable<User[]>;  
  16.   
  17.   constructor(private store: Store) {  
  18.   }  
  19.   
  20.   ngOnInit() { }  
  21.   
  22.   deleteUser(id: number) {  
  23.     this.store.dispatch(new DeleteUser(id));  
  24.   }  
  25.   
  26.   editUser(payload: User) {  
  27.     this.store.dispatch(new SetSelectedUser(payload));  
  28.   }  
  29.   
  30.   
  31. }  

Create model

 
In src -> app, create new folder called models.
 
Inside that folder create a new file called user.ts
 
Put following code in user.ts file 
  1. export interface User {  
  2.     id: number;  
  3.     name: string;  
  4.     city: string;  
  5. }  

Create Actions

 
Inside src > app, create a new folder action. Inside this folder, create a new file user.action.ts.
 
Add following code to user.action.ts 
  1. import { User } from '../models/user';  
  2.   
  3. export class AddUser {  
  4.     static readonly type = '[User] Add';  
  5.   
  6.     constructor(public payload: User) {  
  7.     }  
  8. }  
  9.   
  10. export class UpdateUser {  
  11.     static readonly type = '[User] Update';  
  12.   
  13.     constructor(public payload: User, public id: number) {  
  14.     }  
  15. }  
  16.   
  17. export class DeleteUser {  
  18.     static readonly type = '[User] Delete';  
  19.   
  20.     constructor(public id: number) {  
  21.     }  
  22. }  
  23.   
  24. export class SetSelectedUser {  
  25.     static readonly type = '[User] Set';  
  26.   
  27.     constructor(public payload: User) {  
  28.     }  
  29. }  

Create States

 
Inside src > app, create a new folder states. Inside this folder, create a new file user.state.ts.
 
Add following code to user.state.ts
  1. import { User } from '../models/user';  
  2. import { State, Selector, Action, StateContext } from '@ngxs/store';  
  3. import { AddUser, UpdateUser, DeleteUser, SetSelectedUser } from '../actions/user.action';  
  4. import { Injectable } from '@angular/core';  
  5.   
  6. export class UserStateModel {  
  7.     Users: User[];  
  8.     selectedUser: User;  
  9. }  
  10.   
  11. @State<UserStateModel>({  
  12.     name: 'Users',  
  13.     defaults: {  
  14.         Users: [],  
  15.         selectedUser: null  
  16.     }  
  17. })  
  18.   
  19. @Injectable()  
  20. export class UserState {  
  21.   
  22.     constructor() {  
  23.     }  
  24.   
  25.     @Selector()  
  26.     static getUserList(state: UserStateModel) {  
  27.         return state.Users;  
  28.     }  
  29.   
  30.     @Selector()  
  31.     static getSelectedUser(state: UserStateModel) {  
  32.         return state.selectedUser;  
  33.     }  
  34.   
  35.     @Action(AddUser)  
  36.     addUser({getState, patchState}: StateContext<UserStateModel>, {payload}: AddUser) {  
  37.         const state = getState();  
  38.         const UserList = [...state.Users];  
  39.         payload.id = UserList.length + 1;  
  40.         patchState({  
  41.             Users: [...state.Users, payload]  
  42.         });  
  43.         return;  
  44.     }  
  45.   
  46.     @Action(UpdateUser)  
  47.     updateUser({getState, setState}: StateContext<UserStateModel>, {payload, id}: UpdateUser) {  
  48.         const state = getState();  
  49.         const UserList = [...state.Users];  
  50.         const UserIndex = UserList.findIndex(item => item.id === id);  
  51.         UserList[UserIndex] = payload;  
  52.         setState({  
  53.             ...state,  
  54.             Users: UserList,  
  55.         });  
  56.         return;  
  57.     }  
  58.   
  59.   
  60.     @Action(DeleteUser)  
  61.     deleteUser({getState, setState}: StateContext<UserStateModel>, {id}: DeleteUser) {  
  62.         const state = getState();  
  63.         const filteredArray = state.Users.filter(item => item.id !== id);  
  64.         setState({  
  65.             ...state,  
  66.             Users: filteredArray,  
  67.         });  
  68.         return;  
  69.     }  
  70.   
  71.     @Action(SetSelectedUser)  
  72.     setSelectedUserId({getState, setState}: StateContext<UserStateModel>, {payload}: SetSelectedUser) {  
  73.         const state = getState();  
  74.         setState({  
  75.             ...state,  
  76.             selectedUser: payload  
  77.         });  
  78.         return;  
  79.     }  
  80. }   
Now, save everything and start the application with npm start / ng serve.
 
Open your browser and go to http://localhost:4200

You should be able to see the application as follows:
 
State management in angular with NGXS
 
Fill in the name and city and click save.
 
The new record would be seen in the right-hand side table.
 
State management in angular with NGXS
 
Similarly, you can add multiple users.
 
Also, users can be edited / deleted with the help of the buttons provided in the users list table.
 

Working

 
The Store is a global state manager that dispatches actions your state containers listen to and provides a way to select data slices out from the global state.
 
We injected the store into our components, and used it for dispatching required actions.
 
State management in angular with NGXS
 
Actions can either be thought of as a command which should trigger something to happen, or as the resulting event of something that has already happened.
An action may or may not have metadata (payload). 
 
In our application, we have used various actions for adding users, updating and deleting them, etc.
 
State management in angular with NGXS
 
States are classes that define a state container. Our states listen to actions via an @Action decorator.
 
State management in angular with NGXS
 
We have also used Select.
 
Selects are functions that slice a specific portion of state from the global state container. 
 
State management in angular with NGXS
 
State management in angular with NGXS
 
Hope this is helpful. If I have missed anything, let me know in the comments and I'll add it in!
 
You can get this application on my gihub repository
 
https://github.com/saketadhav/angular11-state-management-with-ngxs-app
 
Happy Coding!