State Management in Angular Using NGXS [Step-by-Step Guide]

NGXS is a state management pattern for the Angular framework. NGXS is a single source of truth for your application’s state, providing simple rules for predictable state mutations. The concept is similar to a Redux library mainly used in React applications.

You can not directly change the state; you must 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.

Here is the step-by-step guide to implement state management using NGXS in Angular.

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

Angular NGXS Tutorial With Example

Step 2: Install NGXS Store.

Next, we’ll install the ngxs store.

yarn add @ngxs/store

Angular NGXS Store

Install the logger-plugin and the dev tools-plugin as a development dependency.

yarn add @ngxs/logger-plugin @ngxs/devtools-plugin --dev

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

 

NGXS Example in Angular

You can see the logs, so when the state changes, we can see its old and new values.

Step 3: Create components.

Inside the src >> app folder, create one folder called components.

We will create and display user information like name and email. We do not make a backend to store the user information. We must 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

Angular NGXS Example

Install Bootstrap 4 using the following command.

yarn add bootstrap

Add the following code inside the src >> styles.css file.

@import "~bootstrap/dist/css/bootstrap.min.css"

Inside src >> app >> components >> create a folder, we need to add some HTML code inside the 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>

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: http://localhost:4200/. You can see something like this below.

NGXS State Management Library

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
  ],

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

We have defined the action to save the user data in 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.

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.

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

}

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>

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.

Angular State Management Pattern

That’s it!

Github Code

6 thoughts on “State Management in Angular Using NGXS [Step-by-Step Guide]”

  1. well explained tutorial, thanks
    1 suggestion : Starting Step 2, we should ‘cd ng6xs’ before running ‘yarn add @ngxs/store’

    Reply
  2. 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.

    Reply
  3. 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

    Reply
  4. This tutorial is nice, but I don’t get why you redefine a select from constructor instead of just using the one you defined through @Selector.

    In my comprehension, this very selector should also always priorized as it is memoized.

    Reply

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.