DEV Community

Cover image for Migrating your web application to NGXS state management
Yonatan Korem
Yonatan Korem

Posted on • Edited on

Migrating your web application to NGXS state management

What's state management?

State management is the concept of having the state of your application be decoupled from the logic or UI of your application, but also not having multiple copies of your data. That saves you from having to sync your data, and allows for an application that will be more consistent and have less defects.

One of the most popular state management patterns is REDUX which emphasizes reactive programming. With this pattern, you have a "single source of truth" and your application observes that data. When it changes, your application reacts to that change as needed by the specific component.

What's NGXS?

NGXS is a front end state management framework for Angular. It's similar to the popular NgRx framework, but offers a lower learning curve, and with it your code contains less boilerplate code - which is something that plagues NgRx.

In NGXS there are three basic concepts to understand before you start integrating it to your code.

Actions

The action is an object that represents a single notification to the store that something happened. For example, an action like SaveUserPreferences would be dispatched when the user clicks on the "Save" button.
An action also has an optional payload which will be readable by any action handler.

class SaveUserPreferences {
    static readonly type = '[Preferences] UserLevel.Save'
    public constructor(public payload: Preferences) {}
}
Enter fullscreen mode Exit fullscreen mode

State

The state class is responsible for handling the partial state of the application. It contains an instance of the state model, and action handlers for whichever actions you want.
The action handlers can modify the state model and/or dispatch more actions.

The first part is the state model:

export interface PreferencesStateModel {
    userLevel: Preferences,
    systemLevel: Preferences
}
Enter fullscreen mode Exit fullscreen mode

The second part is the state itself:


@State<PreferencesStateModel>({
    name: 'PreferencesState', // The name can be used to get the state
    defaults: { // The initial value of the state
        userLevel: {},
        systemLevel: {}
    }
})
export class PreferencesState {
  constructor(private prefService: PreferencesService) {}

  @Action(SaveUserPreferences)
  savePreferences(context, action) {
    return this.prefService.save(action.payload).pipe(
      tap(() => context.dispatch(new LogSuccessfulSave()))
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

Store

The store is an injectable singleton that will be the interface of your application with the state. Your template will observe parts of the state, and your logic will dispatch actions through it.

class Component {
    @Select(PreferenceState) state$: Observable<PreferenceStateModel>

    constructor(private store: Store) { }

    clickHandler() {
        this.store.dispatch(new SaveUserPreferences(this.preferences));
}
Enter fullscreen mode Exit fullscreen mode

Introduction over, let's get to work

tl;dr - The end result

There I was, a new framework all to myself, but nothing to do with it yet. Cue cartoon lightbulb: instead of doing a bunch of work just to setup some mock website, instead I could migrate something to NGXS. What better for an Angular framework than the Angular tutorial - Tour of Heroes.

The app has three pages:

  • The Dashboard Containing a partial list of heroes and a search bar to find a hero by name.
  • The Heroes Containing the list of all heroes, the ability to delete them, and an input field to add new heroes to the database.
  • The Hero Displays the information of a specific hero, with the option to change it.

Each component has its own data, each loads "from the server" upon loading, each using the HeroService to perform actions.
One page even uses the Angular ActivatedRoute and Location objects to read the query parameters, and to navigate to other URLs.

I wanted to reach the following:

  • All data of the website is contained within the NGXS store.
  • All components use only Actions for the actions the user can perform.

My method is simple: work incrementally, and continuously test against the current implementation. I will take one component and slowly rip out its data and service usage, and replace it with the store and its actions.

How did it go?

I picked the Heroes page, since it is the most straightforward. A list of heroes, add a hero, and delete a hero.
Originally, when the component loads, it performs "GET" via a service, and stores the result locally.
Instead, I've defined a GetHeroes action:

// hero.actions.ts
export class GetHeroes {
    readonly type = '[Heroes] Get Heroes'
}
Enter fullscreen mode Exit fullscreen mode

defined the Heroes state to include a list of heroes, and the action handler that performs the GET and stores the result in the state.

@State<HeroStateModel>({
  name: HERO_STATE_TOKEN,
  defaults: {
    heroes: []
  }
})
export class HeroState {
  constructor(private heroService: HeroService) {}

@Action(GetHeroes)
  getHeroes(ctx: StateContext<HeroStateModel>) {
    return this.heroService.getHeroes().pipe(
       tap(heroes => ctx.patchState({ heroes })
    );
  }
Enter fullscreen mode Exit fullscreen mode

Now the component dispatches the action and "selects" the list from the store. The component template looks at the value of the observable and displays it.

export class HeroesComponent implements OnInit {
  @Select(HeroState.heroes) heroes$: Observable<Hero[]>;

  constructor(private store: Store) {}

  ngOnInit() {
      this.store.dispatch(new GetHeroes());
  }
}
Enter fullscreen mode Exit fullscreen mode

BAM!

Did the same for the Add and Delete: Create the actions, dispatch from the component, handle by calling the service and updating the state according to the result.

BAM!

Without much work, the data and logic were completely decoupled from the component. The service wasn't changed at all, and each handler is incredibly focused on what it needs to do.
I then noticed that the Add handler and Get handler both write to the state. Not good! I created a StoreHeroes action and now the two handlers do even less. Call the API and dispatch a new action with the results.

DOUBLE BAM!

Up to here, using NGXS was amazingly simple, had very little boilerplate code, and resulted in highly decoupled code.

One down, two to go

The easiest page done, I decided to go with the Dashboard next. It will be similar to the Heroes page since it also takes the complete heroes list, only this one manipulates it a little.

Inject the store. Select the heroes stream. Create a secondary stream by mapping the complete list, to the first four items in it. Replace the *ngFor to iterate over the stream instead, and...

KABLAM!

The UI was already set to have it be clickable, and when clicking on a hero, it would route to it's page. That part just worked because I only changed the way the information was bound to the template. It was still the same exact structure. I didn't like having the template handling the route, but I decided to get to that later.

Next step was replacing the search. This would be the first time things were not trivial.
I thought: "Easy... I'll take the complete list and filter it with the search term".
But when I looked at the existing code, I noticed that the search is performed via a server call (or at least a mock server call since it is all in-memory).
Usually, I would take the search term, dispatch an action with it, and wait on the response to populate the state. Since the user can manipulate the search term before the response arrives, that means multiple actions can be dispatched. Luckily, NGXS allows to specify "abort this action handle if another action is dispatched".

@Action(HeroSearch, { cancelUncompleted: true })
  searchHero(ctx: StateContext<HeroStateModel>, action: HeroSearch) {
    if (!action.searchToken) {
      return ctx.dispatch(new ClearSearchResults());
    }
    return this.heroService.searchHeroes(action.searchToken).pipe(
      tap(heroes => ctx.patchState({ heroesSearchResults: heroes }))
   );
  }
Enter fullscreen mode Exit fullscreen mode

Last one...

The hero details page was the most complex (which was not that complex) because it was the only one that enabled the user to modify fields of a single hero. That meant that I couldn't just use the value from the store directly.
I also didn't have the hero to display in the store yet.

The original component would read the hero ID from the route, fetch it with the service, and store it locally for modifications.
When you selected a hero via the search, dashboard, or heroes page, the template would route you to a different URL and put the requested hero ID in it.
But I don't want my components to do that stuff. They should be as "dumb" as possible.
Instead, all the places that changed the route would now dispatch a new action:

export class SelectHero {
   static readonly type = '[Hero] Select Hero';
   constructor(public heroId: number) {}
}
Enter fullscreen mode Exit fullscreen mode

The action handler would fetch the hero with the existing service, save it to the store, and then navigate to the detailed view, same as it did before.

@Action(SelectHero)
selectHero(ctx: StateContext<HeroStateModel>, action: SelectHero) {
  return this.heroService.getHero(action.heroId).pipe(
    tap(hero => ctx.patchState({ selectedHero: hero })),
    tap(hero => this.router.navigate([`/detail/${hero.id}`]))
  );
}
Enter fullscreen mode Exit fullscreen mode

Now the component doesn't need to load anything. The selected hero will already be in the store when the route is changed. All it needs to do is select it from the state. As I mentioned before, to enable editing, the component would need a copy of selected hero. To do that, I just need to subscribe to the stream and save a copy with a tap operator

this.hero$.pipe(
   tap(hero => this.heroCopy = { ...hero }) // shallow clone here is enough
).subscribe();
Enter fullscreen mode Exit fullscreen mode

When the user edits the hero and clicks save, another action will be dispatched - UpdateHero. The action handler will do the actual work and the state will be updated accordingly.

Only one thing left: You could manually route directly into the detailed view without ever having the SelectHero action dispatched. To fix that, the component will still take the ID from the route and dispatch the SelectHero with it, but the action handler will ignore it if that ID is already the selected hero.

@Action(SelectHero)
  selectHero(ctx: StateContext<HeroStateModel>, action: SelectHero) {
    if (ctx.getState().selectedHero?.id === action.heroId) {
      return; // Ignore it. This hero is already selected
    }
    return this.heroService.getHero(action.heroId).pipe(
      tap(hero => ctx.patchState({ selectedHero: hero })),
      tap(hero => this.router.navigate([`/detail/${hero.id}`]))
    );
  }
Enter fullscreen mode Exit fullscreen mode

KABLAMO!

With that, I was done. No component injected any service, all the operations were done via action dispatching, and the entire application state was in the store.
(There was a bit more that could have been done with the message logging, but that felt trivial at this point in the exercise)

Lessons Learned

The incremental approach to migration works well

Especially for state managements where you can slowly add to the state. Starting by defining the migration goals, studying the application, and defining a roadmap, made the process work great.

NGXS has a learning curve

But it is fairly slight curve. The straightforward usage of NGXS is simple and you can start using it pretty well. When you try to get complicated, you'll encounter the finer details of the framework. For example, the fact that the observable returned by the dispatch method will emit the state when the action completes, not the value from the async operation that happens in it.

The NGXS router plugin is limited (at the time of writing)

At some point, I wanted to get rid of the use of the ActivatedRoute and the Router and replace them with a NGXS plugin.
While it was great for navigation and getting parameters passed through the URL, the "back" functionality that exist in the "location" object did not. While it can be extended, I just felt it was not worth the trouble.

Tour of heroes is a good starting point, but...

There are a lot of other features in NGXS that proved unnecessary for this project. The entire action life cycle is a huge feature that does not exist at all in NgRx, that can save a lot of boilerplate code when you want to know if a specific action completed and did it succeed.

Hope you've found this article as helpful as I found it interesting to do.

Top comments (0)