Tools We Love – NGXS

  • Product Reviews

Senior software engineer Don C Varghese explains how NGXS helped an AOT Technologies team to centralize application-state tracking in a mobile app, greatly improving data management and resilience.

Moduurn Mobility’s mobile ordering app lets customers use a mobile or web point-of-sale (POS) system to make orders online. Clients for the app might include businesses that deal with customers directly, such as restaurants, grocery stores, coffee shops, and hotels. Moduurn can provide the website or mobile app that serves as the interface between client and customer, or the client can supply their own. A back-office feature allows for customization of items on offer, such as a restaurant’s menus. 

Unfortunately, the app would frequently crash because of inconsistent application states, which was hard to solve because the values of state variables were scattered across the app. As a software engineer, I decided I needed to maintain the application state in a central location with minimal strain on the application’s resources. I realized that a long-term solution for the problem would be to consolidate the application state’s variables in a central store, and so would be available to all components in a well-structured way. The solution I decided upon was to use NGXS, a system to gather and manage states of an Angular app. Here I’ll describe how I diagnosed the problem and how I used NGXS to solve it.

Centralizing the State

A computer program is a sequence of instructions that keeps changing the state of some characteristic of the system. In other words, a system is stateful if it remembers preceding events or user interactions, with the remembered information called the state of the system. A stateless system is a system whose state never changes.

In general, any software system can be considered as a collection of objects that keep interacting with each other. Most of these interactions leave the application in a new state. One of the major challenges in software development is managing the application state. Typically, application state is spread across objects and/or functions, and this diffusion may lead to potential data inconsistency. For example, a developer may use the same name for a global and a local object, and then be surprised not to see global changes after changing the state of the local object.

If we are able to keep the state in a shared place we are able to track the changes happening over the course of time. Systematic management of application state should ease the life of any development team. 

Moduurn’s app is written in Angular, a popular platform for building mobile and web applications. Angular has stateful and stateless components. To share the data/state among components we normally used @Input and @Output decorators, but when the application size became larger, sharing code through these decorators became overkill because @Input/@Output relies on setting up parent-child-grandchild components. When you have routing and sibling components, these decorators become unreliable as two-way communication becomes impossible. Moreover, managing these parent-child-grandchild structures soon became too complicated. Because of these limitations with Angular we switched to using services for sharing data among components. However, managing application state through services also became a nightmare because we soon had hundreds of services to track, each with a different collection of state variables. 

As we revised Moduurn’s solution we also needed to keep in mind that the application would need to handle data from a wide variety of sources and with a wide variety of content. The types of data included:

  • API data: Configuration data used to communicate with external APIs.
  • User information: The customer’s credentials, phone number, secured payment card tokens, and so on.
  • Agent data: Data about the platform and page type the user employed to access the application, information that enables the app to provide properly customized responses.
  • Business data: Search results such as a list of restaurants or menu items displayed in response to a request made by the user through a search box.
  • UI profile data: The customer’s UI preferences such as minimized windows, expansion of tabs, menu layouts, which Moduurn’s app needs to track.
  • Navigation data: When a user navigates from one page to another, the application keeps track of the navigation details from one page to the next.

In our early development work we relied on local storage, session storage for data persistence, and services for sharing the objects between components. Duplicate object names were a common issue in the code base. Referring to the wrong object would often lead to fatal production issues and regression test failures. 

We can look at an example of local storage that we used to store user details. Please see the following code written in Angular.

this._customerService.getDetails(this.userId).subscribe(result =>{
if (result['status'] == 200) {
var userObject = { 'username': result.userName, 'email': result.email, 'user-id': result.userId };
// Put the object into storage
localStorage.setItem('userObject' , JSON.stringify(userObject ));
}
});
// Retrieve the object from storage

This call takes user details from an API and saves them in local storage. Because of the data type we needed to serialize the data each time we stored and retrieved these details using local storage.

The main issues we faced with local or session storage were:

  • It can only store string data. We needed to serialize everything including data types to store JSON data in local storage, but coders find this approach awkward and inefficient.
  • It is synchronous. This means each local storage operation would have to finish before the next storage operation could start, slowing down the app’s runtime.
  • It limits the amount of data we could store, around 5MB across all major browsers, which is a fairly low limit.
  • Any JavaScript code on our pages could access local storage, so there’d be no data protection whatsoever, potentially a big security issue for the end user.

The other option we considered was to keep the data in services. The main problems with this choice were:

  • Stored data would be lost when refreshing the page.
  • Large amounts of code management would be required.

After extensively discussing the various issues we decided to upgrade the application by using the latest version of Angular along with an appropriate state-management tool. In the end we chose NGXS. Here are some of its basic features.

What is NGXS?

NGXS tries to make things as simple and accessible as possible. For instance, developers are not required to be super familiar with RxJS, a library for composing asynchronous and event-based programs by using observable sequences. It provides one core type (the Observable), satellite types (Observer, Schedulers, Subjects), and operators inspired by Array#extras (map, filter, reduce, every, etc.) to permit handling asynchronous events as collections.

RxJS works well and is extensively used within the project, but a drawback is that the library tries to do as much as it can for you, which is sometimes too much. In contrast, NGXS encourages users to take advantage of Observables, but in many cases treats them as an implementation detail of the library rather than as a prerequisite.

In addition, NGXS gets rid of switch statements. Instead, the library is responsible for knowing when functions need to be called.

How NGXS Works

NGXS is very simple to use compared to other state-management patterns like Redux and Akita. NGXS takes full advantage of Angular and TypeScript instead of using the Redux pattern. 

There are four major concepts behind NGXS. These concepts create a circular control flow that travels from a component that’s dispatching an action to a store reacting to the action, and then returns to the component through a state select.

Let’s explore stores, actions, states, and selects.

1. Stores

The store is a global state manager that dispatches actions your state containers listen to and provides a way to select data slices from the global state.

this.store.dispatch(new AddCar(name));

You can also dispatch multiple actions at the same time by passing an array of actions like:

this.store.dispatch([new AddCar('Ford Figo'), new AddCar('Tata Nexon')])

Here we see how we can easily add a new value to the state variable. We can easily store data with a larger number of data types than we can using local storage. In addition to this, we don’t need to serialize the data, which we usually have to do when using local or session storage.

If we want to perform further actions after the initial action executes, we can use the observable behaviour of the dispatch function:

this.store.dispatch(new AddCar(name)).subscribe(() => this.form.reset());

2. Actions

An action is a type of command that should be called when some operation happens or we want to trigger for some event like adding a new Car or listing all Cars.

export interface Car {
    name: string;
}
 
export class AddCar {
      static readonly type = '[Car] Add';
      constructor(public payload: Car) { }
}

Sometimes we may not want the completion of an action to wait for the asynchronous work to complete. To avoid the delay, we can simply choose not to return the handle to our asynchronous work from the @Action method. Note that in the case of an Observable we would have to .subscribe(…) or call .toPromise() to ensure that our Observable runs.

Observable version:

@Action(GetCars)
getCars(ctx: StateContext) {
  this.carService.getCars().subscribe(cars => {
    ctx.patchState({ cars});
  });
}

Promise version:

@Action(GetCars)
getNovels(ctx: StateContext) {
  this.carService.getCars().toPromise()
    .then(cars => {
      ctx.patchState({ cars });
    });
}

3. States

States are classes and decorators that can be used to describe metadata and action mappings. Here’s an example:

import { Injectable } from '@angular/core';
import { State } from '@ngxs/store';
 
@State<CarStateModel[]>({
    name: 'carList',
    defaults: {
        carList: [],
    },
})
@Injectable()
export class CarState {
 
@Action(Add)
@ImmutableContext()
 
public Add({ setState }: StateContext, { payload }: Add): void {
 
    setState((state: CarStateModel) => ({
      state.carList.push(payload);
      return state;
    }));
  }
}

Next, we see how the state decorator provides metadata for the state. The name property indicates the name of the state to slice. Note that name is a required parameter and must be unique across the application, while defaults is a set of objects or arrays for this state slice. These states and decorators let us introduce declarative updates, which improves the readability of our code. For instance, we can write simple code even if we’re working with deep nesting.

We also see here how states can support dependency injection. This is hooked up automatically so all you need to do is inject your dependencies in the constructor:

@State<CarStateModel[]>({
    name: 'carList',
    defaults: {
        carList: [],
    },
})
@Injectable()
export class CarState {
  constructor(private carService: CarService) {}
}

4. Selects

Selects are functions that extract a specific slice of the global state container so we can get the data from a specific global state whenever we want to. In this way the Action method can return not only an Observable, but also a Promise.

@Select(CarState) cars$: Observable<string[]>;

In the store, there is a selectSnapshot function that allows us to pull out the raw value. This is helpful for cases where we need to get a static value but can’t use Observables.

const token = this.store.selectSnapshot((state: CarState) => state.car.name);

Before NGXS

We can do a quick comparison between our version 1 code and the revised application code that uses NGXS. Consider the following scenario: we need to get the email of a user that is already stored in the local or session storage or service. First, let’s look at our pre-NGXS code:

Local Storage:

export class AppComponent extends AppBaseComponent {
constructor()
{
var userObject = localStorage.getItem(userObject);
userObject  = JSON.parse(userObject);
var email = userObject.email; 
}

Session Storage:

export class AppComponent extends AppBaseComponent {
constructor()
{
var userObject = sessionStorage.getItem(userObject);
userObject  = JSON.parse(userObject);
var email = userObject.email; 
}

Local Service:

class User{
    private _email: boolean = false;
get email(): string{
        return this._email;
    }
    set email(value: string) {
        this._email = value;
    }
}
// To access the data
export class AppComponent extends AppBaseComponent {
constructor()
{
var user = new User();
if(user.email) {
let email = user.email;
}

Using NGXS

And here we see how we could greatly simplify our code using NGXS:

constructor(private store: Store)
{
let email = this.store.selectSnapshot((state: UserState) => state.user.email); 
}

Conclusion

By implementing NGXS as a state-management tool for our Moduurn project, all variables of the relevant application state are saved in one location. This centralization makes it easier to track down problems, as we can easily obtain a snapshot of the state to provide important insight into an error, allowing us to easily recreate issues. Moreover in our revised code we no longer needed to worry about various issues that arise when relying on local storage and session storage. These changes improved the performance of the application as well as simplified the coding, making the code much lighter and understandable for the reader. In addition, we can use promises and async/await syntax throughout the project because NGXS offers more reliable error-handling mechanisms and control of asynchronous operations. We can also easily store a large amount of data compared to local or session storage and we don’t need to worry about the size limit. 

A big advantage using NGXS is that we can store our data using proper data types and completely avoid manual serialization and deserialization of the object while saving or retrieving. Moreover, the scope of objects and properties remains well defined through the use of NGXS. This greatly eases data management and keeps the application more resilient in the face of any syntax or data-redundancy errors.

We have gone through some basic concepts of NGXS and seen how its management of application states greatly improved the reliability and efficiency of the application we worked on for Moduurn Mobility. We’ve covered only a few features. For more details you can consult this NGXS page to read about advanced features and plugs such as Shared State and Meta Reducers.

About the Author

Don C Varghese works as a senior software engineer at AOT Technologies. His areas of expertise include Angular, Node.js, AWS, and MongoDB. He specializes in integrations and payment gateways.