The official documentation for the NGXS library is here. We have covered the Angular NgRx Store pattern in this blog, and now it is time for an alternative to this library called Angular NGXS.
What is NGXS
NGXS is a state management pattern for the Angular framework. NGXS acts as a single source of truth for your application’s state, providing simple rules for predictable state mutations. The concept is the same as a Redux library mostly used in React applications.
You can not directly change the state; you need to commit the action and then modify the state. NGXS is modeled after the CQRS pattern popularly implemented in libraries like Redux and NGRX but reduces boilerplate using modern TypeScript features such as classes and decorators.
Flow of NGXS
The flow of NGXS is effortless.
- The angular component dispatches the action.
- If the action needs to change the database values, it sends a network request to the server and performs the database operations.
- Then after getting a response from the server, it will mutate the state. If the backend is not there, then action directly mutates the store’s state, and the component select that state and updates the UI. It is the same cycle as Redux if you are familiar with it. You can find more here.
Angular NGXS
Angular Ngxs is a different approach to state management. NGXS acts as a single source of truth for your application’s state, providing simple rules for predictable state mutations.
We start this demo example by installing Angular using Angular CLI.
Step 1: Install Angular.
If you have not previously installed Angular CLI globally on your machine, install it using the following command.
npm install -g @angular/cli # or yarn add global @angular/cli
Create an Angular project using the following command.
ng new ng6xs
Step 2: Install NGXS Store.
Next, we’ll install the ngxs store.
yarn add @ngxs/store
Now install logger-plugin and the dev tools-plugin as a development dependency.
yarn add @ngxs/logger-plugin @ngxs/devtools-plugin --dev
Now, import these modules inside an app.module.ts file.
// app.module.ts import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { NgxsModule } from '@ngxs/store'; import { NgxsReduxDevtoolsPluginModule } from '@ngxs/devtools-plugin'; import { NgxsLoggerPluginModule } from '@ngxs/logger-plugin'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, NgxsModule.forRoot(), NgxsReduxDevtoolsPluginModule.forRoot(), NgxsLoggerPluginModule.forRoot() ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
Type the following command to start the Angular Development server.
ng serve -o
You can see the logs, so when the state changes, we can its old and new values.
Step 3: Create components.
Inside src >> app folder, create one folder called components.
Now, we will create and display user information like name and email. We do not make a backend to store the user information. We need to add the data to the store and display data on the Angular Frontend.
We will create two components, so type the following command to generate the Angular components.
ng g c components/create --spec=false ng g c components/index --spec=false
Also, install Bootstrap 4 using the following command.
yarn add bootstrap
Now, add the following code inside src >> styles.css file.
@import "~bootstrap/dist/css/bootstrap.min.css"
Inside src >> app >> components >> create folder, we need to add some HTML code inside create.component.html file.
<div class="card"> <div class="card-body"> <form> <div class="form-group"> <label class="col-md-4">Name</label> <input type="text" class="form-control" #name/> </div> <div class="form-group"> <label class="col-md-4">Email</label> <input type="email" class="form-control" #email/> </div> <div class="form-group"> <button (click)="addUser(name.value, email.value)" class="btn btn-primary">Create User</button> </div> </form> </div> </div>
Now, add this component inside an app.component.html file.
<div class="container"> <div class="row"> <div class="col-md-6"> <app-create></app-create> </div> <div class="col-md-6"></div> </div> </div>
Save the file and go to the: http://localhost:4200/. You can see something like this below.
Okay, now we need the ReactiveFormsModule. We will use the Reactive approach to the form and not the template-driven approach. So Inside the app.module.ts file, add the ReactiveFormsModule from @angular/forms package.
// app.module.ts import { ReactiveFormsModule } from '@angular/forms'; imports: [ BrowserModule, NgxsModule.forRoot(), NgxsReduxDevtoolsPluginModule.forRoot(), NgxsLoggerPluginModule.forRoot(), ReactiveFormsModule ],
Now, write the following code inside a create.component.ts file.
// create.component.ts import { Component, OnInit } from '@angular/core'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.css'] }) export class CreateComponent implements OnInit { angForm: FormGroup; constructor(private fb: FormBuilder) { this.createForm(); } createForm() { this.angForm = this.fb.group({ name: ['', Validators.required ], email: ['', Validators.required ] }); } addUser(name, email) { console.log(name, email); } ngOnInit() { } }
And finally, write the html code inside a create.component.html file.
<div class="card"> <div class="card-body"> <form [formGroup]="angForm"> <div class="form-group"> <label class="col-md-4">Name</label> <input type="text" class="form-control" formControlName="name" #name/> <div *ngIf="angForm.controls['name'].invalid && (angForm.controls['name'].dirty || angForm.controls['name'].touched)" class="alert alert-danger"> <div *ngIf="angForm.controls['name'].errors.required"> Name is required. </div> </div> </div> <div class="form-group"> <label class="col-md-4">Email</label> <input type="email" class="form-control" formControlName="email" #email/> <div *ngIf="angForm.controls['email'].invalid && (angForm.controls['email'].dirty || angForm.controls['email'].touched)" class="alert alert-danger"> <div *ngIf="angForm.controls['email'].errors.required"> Email is required. </div> </div> </div> <div class="form-group"> <button (click)="addUser(name.value, email.value)" class="btn btn-primary" [disabled]="angForm.pristine || angForm.invalid">Create User</button> </div> </form> </div> </div>
Step 4: Define a model.
Inside src >,> app folder, create one folder called models and create one file called User.ts.
// User.ts export interface User { name: string; email: string; }
Step 5: Define Actions.
We will create the addUser action. Inside src >> app folder, create one folder called actions. Inside the actions folder, create one file called user.action.ts.
// user.action.ts import { User } from '../models/User'; export class AddUser { static readonly type = '[User] Add'; constructor(public payload: User) {} }
Step 6: Defining a State.
A key difference between Ngrx and Ngxs is how the state is handled. The state file in Ngxs takes the place of reducers in Ngrx. This is done by utilizing various decorators.
Inside the src >> app folder, create one folder called the state, and in that folder, create one file called user.state.ts.
Write the following code inside the user.state.ts file.
// user.action.ts import { State, Action, StateContext, Selector } from '@ngxs/store'; import { User } from '../models/User'; import { AddUser } from '../actions/user.action'; export class UserStateModel { users: User[]; } @State<UserStateModel>({ name: 'users', defaults: { users: [] } }) export class UserState { @Selector() static getUsers(state: UserStateModel) { return state.users; } @Action(AddUser) add({getState, patchState }: StateContext<UserStateModel>, { payload }: AddUser) { const state = getState(); patchState({ users: [...state.users, payload] }); } }
Here, we have defined the action to save the user data into the store. When the user tries to create a new user, we get those payload values here and add them to the user’s state array.
So, when the user is created, the store will update its user state, and another component fetches that state. In our case, it is index.component.ts.
So it will change its UI and display the newly created user.
Finally, import the store inside a create.component.ts file.
// create.component.ts import { Component, OnInit } from '@angular/core'; import { Store } from '@ngxs/store'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { AddUser } from '../../actions/user.action'; @Component({ selector: 'app-create', templateUrl: './create.component.html', styleUrls: ['./create.component.css'] }) export class CreateComponent implements OnInit { angForm: FormGroup; constructor(private fb: FormBuilder, private store: Store) { this.createForm(); } createForm() { this.angForm = this.fb.group({ name: ['', Validators.required ], email: ['', Validators.required ] }); } addUser(name, email) { this.store.dispatch(new AddUser({ name, email})); } ngOnInit() { } }
Step 7: Updating the app.module.ts file.
// user.state.ts import { UserState } from './state/user.state'; imports: [ BrowserModule, NgxsModule.forRoot([ UserState ]), NgxsReduxDevtoolsPluginModule.forRoot(), NgxsLoggerPluginModule.forRoot(), ReactiveFormsModule ],
Step 8: Display the data.
Write the following code inside the index.component.ts file.
// index.component.ts import { Component, OnInit } from '@angular/core'; import { Store, Select } from '@ngxs/store'; import { User } from '../../models/User'; import { Observable } from 'rxjs'; @Component({ selector: 'app-index', templateUrl: './index.component.html', styleUrls: ['./index.component.css'] }) export class IndexComponent implements OnInit { users: Observable<User>; constructor(private store: Store) { this.users = this.store.select(state => state.users.users); } ngOnInit() { } }
Here, if the user’s state array is changed, this component rerenders and displays the changes. So, for example, if the new user is added, this component rerenders and shows the new user.
Also, write the HTML code inside an index.component.html file.
<div *ngIf="users"> <table class="table table-striped"> <thead> <tr> <th>Name</th> <th>Email</th> </tr> </thead> <tbody> <tr *ngFor="let user of users | async"> <td>{{ user.name }}</td> <td>{{ user.email }}</td> </tr> </tbody> </table> </div>
Now, include this component inside an app.component.html file.
<div class="container"> <div class="row"> <div class="col-md-6"> <app-create></app-create> </div> <div class="col-md-6"> <app-index></app-index> </div> </div> </div>
Save the file and go to the browser. You can see below.
Finally, the Angular 12 NGXS Example is over. Thanks for taking it.
Thank you for the wonderful and detailed walkthrough
well explained tutorial, thanks
1 suggestion : Starting Step 2, we should ‘cd ng6xs’ before running ‘yarn add @ngxs/store’
what about a full CRUD example?
Hello,
I have a conceptual problem. I like and think I understand your demo, but my question is this. I need to implement a workflow of various states. Does that mean I need to define multiple state classes? Some states could lead to others based on the users actions. All the examples on the web has a single Store and a single state. So I am confused as to how to get multiple identifiable states based on a user’s actions. For example New, Pending Submitted states. New is an initial state, when there is no record in the database, Pending is when there is a record in the db, so the entry has an id, but needs more information in the database to be complete. Submitted is when the entry fields are properly filled up, validated and submitted to the database. At any stage I could trigger a Remove action which would put the record in removed state.
Hi Krunal,
Thanks for an excellent beginner tutorial. Do you have an advanced tutorial?
Just a note: your web site comes up with an ad that says your computer is infected.
Raman