DEV Community

Cover image for Beginner's guide to State Management using NGXS
siddheshthipse
siddheshthipse

Posted on • Edited on

Beginner's guide to State Management using NGXS

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.

Block Schematic of NGXS

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 { }



Enter fullscreen mode Exit fullscreen mode

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]


Enter fullscreen mode Exit fullscreen mode

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) { }
}




Enter fullscreen mode Exit fullscreen mode

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 named payload, 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);
  }
}


Enter fullscreen mode Exit fullscreen mode

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
            })
        }))
    }
}


Enter fullscreen mode Exit fullscreen mode
About Selector()
  • The Selector() is used to get a specific piece of data from the AppState.
  • In our case we are grabbing the users array which is present inside the AppState
  • 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()


Enter fullscreen mode Exit fullscreen mode

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));
  }
}


Enter fullscreen mode Exit fullscreen mode
Few Important Points
  • Import select and store from ngxs/store
  • The Select() is basically used to grab the data present in the AppState.
  • 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 passing i (userid) as a parameter.
  • So that the user having userid equal to i will get deleted from the AppState.

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>


Enter fullscreen mode Exit fullscreen mode

Landing Page UI

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)

Collapse
 
briancodes profile image
Brian

Is the naming of methods correct here e.g. getDataFromState(....), would that be better as getUsers(...), as it's the selector that gets the data from the state?

Collapse
 
siddheshthipse profile image
siddheshthipse

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