NGXS — Initial impressions of an Angular state management solution

Most of my frontend work is done in Angular but I make it a point to get familiar with other frameworks as well, like React for instance. After going through a rough time managing an ever-expanding state with Behavior Subjects on one project, we decided to give the Redux approach a try for building a big business application on a dierent project. While researching whether to use NGRX or NGXS, the current best solutions for Redux like state management for Angular, I got the impression that NGXS has better “integration” with Angular because of its object-oriented nature.

NGXS makes it very easy to get started. The concept is intuitive for people that have used Redux before but is tailored for Angular developers.

A circular control flow traveling from a component dispatching an action, to a store reacting to the action, back to the component through a state select.

This is not a NGXS tutorial!

I’m not gonna go through the installation process or write up a demo. I am however gonna give my opinions on some features and talk about my use-cases.

Selects

Selects are functions that slice a specific portion of the state from the global state container. Using them is very useful in an application. They allow you to read state parts of interest from wherever you need, be it a service or a component.

import { Select } from '@ngxs/store';
import { UserState, UserStateModel } from './user.state';

@Component({ ... })
export class UserComponent {

// Reads the name of the state from the state class
@Select(UserState) list$: Observable<User[]>;

// Uses the pandas memoized selector to only return pandas
@Select(UserState.getUsers) user$: Observable<User[]>;

// Also accepts a function like our select method
@Select(state => state.user.list) user$: Observable<User[]>;

// Reads the name of the state from the parameter
@Select() user$: Observable<UserStateModel>;

}

Since the Select is consumed as an Observable it gives a very natural feel inside the Angular ecosystem. It is also great to use with the Angular async pipe.

An interesting use case I have encountered is to allow for a selector to be reused to select from States that have the same structure:

export class SharedSelectors {
  static getEntities(stateClass) {
    return createSelector(
    [stateClass],
    (state: { entities: any[] }) => {
    return state.entities;
    });
    }​
    }

Same structure States

One of the first things you do when you start to develop an application is to manage CRUD functionalities for several entities. Let’s say we start with only stand-alone entities. In NGXS each entity would require a State and in turn a state model. In our case we went for something like this:

interface UserStateModel {
filtered: string[];
item: User;
list: User[];
searchTerm: string;
selected: string[];
}

This structure enables us to keep track of the list of users (list: User[]), which ones are selected by pushing their string identifiers in an array (selected: string[]), which ones match our search term, also by pushing their string identifiers in an array (filtered: string[]), and a single user (item: User) to manage in a form. This was the same for all stand-alone entities we wanted to manage so we created a reusable state model with a generic type:

interface EntityStateModel<T> {
    filtered: string[];
    item: T;
    list: T[];
    searchTerm: string;
    selected: string[];
    }

But this only makes the state model reusable. What about the States them selfs? Each State still needs actions to manage the entity’s state data. Since the State models are pretty much the same, the actions between States will have the same logic as well. Unfortunately, we need to write the “same” action methods for each entity State and link them to actions specific to that entity:

@State<EntityStateModel<user>>({
    name: 'user',
    defaults: {
      // default values
    }
  })
  export class UserState {
    @Action(CreateUser)
    createUser(
      ctx: StateContext<EntityStateModel<user>>,
      action: CreateUser
    ) {
      // Action logic
    }
  }...@State<EntityStateModel<event>>({
    name: 'event',
    defaults: {
      // default values
    }
  })
  export class EventState {
    @Action(CreateEvent)
    createEvent(
      ctx: StateContext<EntityStateModel<event>>,
      action: CreateEvent
    ) {
      // Same action logic
    }
  }</event>></event>></user>></user>>

The more entities you need to manage the more code gets duplicated. To deal with this problem, we created an abstract EntityState to contain all the logic and make each entity State extend it:

@State<EntityStateModel<event>>({
   name: 'event',
    defaults: {
    // default values
    }
    })
    export class EventState extends EntityState<Event> {
    @Action(CreateEvent)
    createEvent(
    ctx: StateContext<EntityStateModel<event>>,
    action: CreateEvent
    ) {
    this._createEntity(ctx, action);
    }
    }</event>></event>>

The _createEntity is a protected method of EntityState that contains the entity-agnostic logic. Now we can just call the entity-agnostic methods in the entity-specific State action methods.

There is an advanced feature called composition but it does not fit this use case. It is used to share actions between States. Since our use case has stand-alone entity States, all the actions are entity-specific.

We are using an advanced feature called SubState to manage nested objects or to reference relationships between stand-alone entity States. We might be interested in what events a certain user is attending. To achieve this we add a UserEventState that has a model similar to the EventState:

interface UserEventStateModel {
    filtered: string[];
    list: string[];
    searchTerm: string;
    selected: string[];
    }

The UserEventState is added as a child state to the UserState and keeps track of the user events in the list array by event string identifiers. To fetch the Event objects from the UserEventState, the Select would then look something like this:

@Select([EventState.getEvents])
static getUserEvents(state, events) {
return events.filter(event => {
return state.list.indexOf(event.id) !== -1;
});
}

This way we keep a single point of truth, which is one of the pillars of Redux architecture.

Conclusion

NGXS is a great tool to manage state in an Angular application. Once you setup your State structures and actions, it is fairly easy to build on. The State is the brain and the components have little to no logic in them. They just call actions which the State resolves and updates the state accordingly. Since the components are subscribed to the state updates via the Selects, the changes are viewed immediately.

We’re having a lot of fun working with NGXS and are still exploring how to manage things. If any of you find this useful be sure to comment, I’ll be glad to answer any questions as well and if anybody sees that we’re doing something wrong, please tell us how to improve.


Leave a Reply

Your email address will not be published. Required fields are marked *