NGXS 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.
Getting started with NGXS as a beginner can be daunting, not because it is some kind of rocket science but essentially due to the fact that not many resources are available to learn it in a right way.
In this tutorial, we will use Angular along with NGXS to create a simple CRUD application consuming dummy REST APIs.
If you're running out of patience already you can hop onto StackBlitz and see for yourself what we're gonna do.
Prerequisites
- Basic knowledge of Angular 2+ is must.
- Prior knowledge of RxJS would be helpful but isn't absolutely necessary.
So let's get started
Step 1: Install Angular CLI
npm install -g @angular/cli
OR
yarn add global @angular/cli
Create a new Angular project, let's call it 'learning-ngxs'
ng new learning-ngxs
Step 2: Install NGXS library
First go to the project folder
cd learning-ngxs
Then enter this command
npm install @ngxs/store --save
or if you're using yarn
yarn add @ngxs/store
Step 3: Installing Plugins(optional)
- Although this step is optional, I would highly recommend you to go through it since Logger and Devtools are the two extremely handy dev dependencies.
- These plugins help us in tracking the changes our state goes through.
For installing Logger and Devtools plugins fire the commands @ngxs/logger-plugin --save
& @ngxs/devtools-plugin --save-dev
respectively.
Step 4: Importing Modules
This is how your app.module.ts
file will look after importing the necessary modules
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import {HttpClientModule} from '@angular/common/http';
import {FormsModule,ReactiveFormsModule} from '@angular/forms';
//For NGXS
import { NgxsModule } from '@ngxs/store';
import { NgxsLoggerPluginModule } from '@ngxs/logger-plugin';
import { NgxsReduxDevtoolsPluginModule } from '@ngxs/devtools-plugin';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { GeneralComponent } from './general/general.component';
import { AppState } from './states/app.state';
import { DesignutilityService } from './designutility.service';
@NgModule({
declarations: [
AppComponent,
GeneralComponent
],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule,
FormsModule,
ReactiveFormsModule,
NgxsModule.forRoot([]), NgxsLoggerPluginModule.forRoot(), NgxsReduxDevtoolsPluginModule.forRoot()
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Step 5: Creating Components and Services
Let's create a component say 'general' for displaying the contents of our state
ng g c general
Create a service called 'designutility' for interacting with the server to GET
, POST
, UPDATE
and DELETE
the data.
ng g s designutility
Do not forget to add DesignutilityService
inside the providers
array in app.module.ts
.
providers: [DesignutilityService]
Make sure that you've imported all the modules mentioned in Step 4.
Step 6: Creating Actions
Create a new folder named 'actions' inside src>app
Inside the actions folder, create a new file named app.action.ts
//Here we define four actions for CRUD operations respectively
//Read
export class GetUsers {
static readonly type = '[Users] Fetch';
}
//Create
export class AddUsers {
static readonly type = '[Users] Add';
constructor(public payload: any) { }
}
//Update
export class UpdateUsers {
static readonly type = '[Users] Update';
constructor(public payload: any, public id: number, public i:number) { }
}
//Delete
export class DeleteUsers {
static readonly type = '[Users] Delete';
constructor(public id: number) { }
}
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.
Each action contains a type field which is its unique identifier.
Actions are dispatched from the components to make the desirable changes to the State.
You might have noticed that except for GetUsers
, in all other actions we have a parameterized constructor.
- These parameters are nothing but the data which would be coming from various components whenever the action is dispatched.
- For example in
AddUsers
action we have a constructor with parameter namedpayload
, this payload will basically comprise of information about the new user. - This data about the newly created user will get stored inside the State whenever the action
AddUsers
is dispatched from the component.
Step 7: Working with Service
In the designutility.service.ts
, let’s add HTTP calls to fetch, update, add and delete to-do items.
In this tutorial, we are using JSONPlaceholder for making fake API calls.
import { Injectable } from '@angular/core';
import {HttpClient} from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class DesignutilityService {
constructor(private http:HttpClient) { }
fetchUsers(){
return this.http.get('https://jsonplaceholder.typicode.com/users');
}
addUsers(userData){
return this.http.post('https://jsonplaceholder.typicode.com/users',userData);
}
deleteUser(id:number){
return this.http.delete('https://jsonplaceholder.typicode.com/users/'+id);
}
updateUser(payload,id:number){
return this.http.put('https://jsonplaceholder.typicode.com/users/'+id, payload);
}
}
Step 8: Creating State
Now we've arrived at the most important part of this tutorial.
Create a new folder named 'states' inside src>app.
Inside the states folder, create a new file named app.state.ts
import { Injectable } from "@angular/core";
import { Action, Selector, State, StateContext } from "@ngxs/store";
import { DesignutilityService } from "../designutility.service";
import { tap } from 'rxjs/operators';
import { AddUsers, DeleteUsers, GetUsers, UpdateUsers } from "../actions/app.action";
export class UserStateModel {
users: any
}
@State<UserStateModel>({
name: 'appstate',
defaults: {
users: []
}
})
@Injectable()
export class AppState {
constructor(private _du: DesignutilityService) { }
@Selector()
static selectStateData(state:UserStateModel){
return state.users;
}
@Action(GetUsers)
getDataFromState(ctx: StateContext<UserStateModel>) {
return this._du.fetchUsers().pipe(tap(returnData => {
const state = ctx.getState();
ctx.setState({
...state,
users: returnData //here the data coming from the API will get assigned to the users variable inside the appstate
})
}))
}
@Action(AddUsers)
addDataToState(ctx: StateContext<UserStateModel>, { payload }: AddUsers) {
return this._du.addUsers(payload).pipe(tap(returnData => {
const state=ctx.getState();
ctx.patchState({
users:[...state.users,returnData]
})
}))
}
@Action(UpdateUsers)
updateDataOfState(ctx: StateContext<UserStateModel>, { payload, id, i }: UpdateUsers) {
return this._du.updateUser(payload, i).pipe(tap(returnData => {
const state=ctx.getState();
const userList = [...state.users];
userList[i]=payload;
ctx.setState({
...state,
users: userList,
});
}))
}
@Action(DeleteUsers)
deleteDataFromState(ctx: StateContext<UserStateModel>, { id }: DeleteUsers) {
return this._du.deleteUser(id).pipe(tap(returnData => {
const state=ctx.getState();
console.log("The is is",id)
//Here we will create a new Array called filteredArray which won't contain the given id and set it equal to state.todo
const filteredArray=state.users.filter(contents=>contents.id!==id);
ctx.setState({
...state,
users:filteredArray
})
}))
}
}
About Selector()
- The
Selector()
is used to get a specific piece of data from theAppState
. - In our case we are grabbing the
users
array which is present inside theAppState
- The selector is used to return the data back to the component with the help of
Select()
as shown in Step 10.
Step 9: Documenting the State in app.module.ts
Now that we are done with the creation of AppState
, it is necessary to document this state in our app.module.ts
file.
So go to imports array inside app.module.ts
and make the necessary change.
NgxsModule.forRoot([AppState]), NgxsLoggerPluginModule.forRoot(), NgxsReduxDevtoolsPluginModule.forRoot()
Step 10: Working with Component
Component is the place from where we're gonna control the contents of the state, in our case it's general.component.ts
We are performing basic CRUD operations on our AppState
.
For that, we have a table to display existing users, update user information, remove user and a form to insert a new user into the AppState
.
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { AddUsers, DeleteUsers, GetUsers, UpdateUsers } from '../actions/app.action';
import { AppState } from '../states/app.state';
@Component({
selector: 'app-general',
templateUrl: './general.component.html',
styleUrls: ['./general.component.css']
})
export class GeneralComponent implements OnInit {
//Here I have used Reactive Form, you can also use Template Driven Form instead
userForm: FormGroup;
userInfo: [];
@Select(AppState.selectStateData) userInfo$: Observable<any>;
constructor(private store: Store, private fb: FormBuilder) { }
ngOnInit(): void {
this.userForm = this.fb.group({
id: [''],
name: [''],
username: [''],
email: [''],
phone: [''],
website: ['']
})
this.store.dispatch(new GetUsers());
this.userInfo$.subscribe((returnData) => {
this.userInfo = returnData;
})
}
addUser() {
this.store.dispatch(new AddUsers(this.userForm.value));
this.userForm.reset();
}
updateUser(id, i) {
const newData = {
id: id,
name: "Siddhesh Thipse",
username: "iamsid2399",
email: 'siddheshthipse09@gmail.com',
phone: '02138-280044',
website: 'samplewebsite.com'
}
this.store.dispatch(new UpdateUsers(newData, id, i));
}
deleteUser(i) {
this.store.dispatch(new DeleteUsers(i));
}
}
Few Important Points
- Import
select
andstore
fromngxs/store
- The
Select()
is basically used to grab the data present in theAppState
. - Notice how we are dispatching various actions to perform the desired operations, for example if we want to delete a user, we are dispatching an action named
DeleteUsers
and passingi
(userid) as a parameter. - So that the user having userid equal to
i
will get deleted from theAppState
.
For designing part I have used Bootstrap 5, but you can totally skip using it if UI isn't your concern as of now.
After creating the basic UI, this is how our general.component.html
will look like
<div class="container-fluid">
<h2 style="text-decoration: underline;">Getting started with NGXS</h2>
<div class="row my-4">
<div class="col-md-3">
<h5 style="color: grey;">Add new user to State</h5>
<form [formGroup]="userForm" (ngSubmit)="addUser()">
<label class="form-label">ID</label>
<input type="text" class="form-control mb-2" placeholder="User ID" formControlName="id">
<label class="form-label">Name</label>
<input type="text" class="form-control mb-2" placeholder="Enter Name" formControlName="name">
<label class="form-label">Username</label>
<input type="text" class="form-control mb-2" placeholder="Enter a unique username" formControlName="username">
<label class="form-label">Email</label>
<input type="email" class="form-control mb-2" placeholder="example@abcd.com" formControlName="email">
<label class="form-label">Phone</label>
<input type="number" class="form-control mb-2" placeholder="Enter Contact No." formControlName="phone">
<label class="form-label">Website</label>
<input type="email" class="form-control mb-2" placeholder="Enter website name" formControlName="website">
<button type="submit" class="btn btn-primary btn-sm mt-2">Add User</button>
</form>
</div>
<div class="col-md-9">
<h5 style="color: grey;">User Information</h5>
<table class="table">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Name</th>
<th scope="col">Username</th>
<th scope="col">Email</th>
<th scope="col">Phone</th>
<th scope="col">Website</th>
<th scope="col">Update</th>
<th scope="col">Delete</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let contents of userInfo; index as i">
<th scope="row">{{contents.id}}</th>
<td>{{contents.name}}</td>
<td>{{contents.username}}</td>
<td>{{contents.email}}</td>
<td>{{contents.phone}}</td>
<td>{{contents.website}}</td>
<td><button class="btn btn-outline-warning btn-sm" (click)="updateUser(contents.id,i)"><i
class="bi bi-pencil-fill"></i></button></td>
<td><button class="btn btn-danger btn-sm" (click)="deleteUser(contents.id)"><i
class="bi bi-trash-fill"></i></button></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
That's it, we have successfully implemented State Management in our Angular Application.
Now there is definitely more to NGXS than this, but once you've completely understood the basics, learning the advanced stuff is a cakewalk.
Incase of any suggestions/queries please feel free to comment down below.
Source code available on Github
Top comments (2)
Is the naming of methods correct here e.g.
getDataFromState(....)
, would that be better asgetUsers(...)
, as it's the selector that gets the data from the state?Thanks for the comment Brian.
Yes you are correct, when you will be dealing with multiple entities in your application you need to be specific in naming
Here since we are dealing with only one entity i.e the user related data, I have given a generic name to the method