Josh Hicks
Feb 20 · 8 min read

How do you eat an elephant?

Disclaimer

If you’re not familiar with modern state management tools this article will probably not be the best place to start. This article assumes that you have done your research on both NGRX and NGXS and are just looking for a guide to migrate things over. I will mention comparison points when relevant, but that is not the purpose of this guide. So, with that disclaimer out of the way, let’s begin!


In order to understand what work will need to be done to migrate from one library to another it may be helpful to see a diagram about how things will map from one to another. Let’s see a quick breakdown of the anatomy of each library.

Diagram comparing NGRX and NGXS

As you can see there is a lot of overlap here. The key differences are handling side-effects, the use of decorators, and state mutations. In NGRX those are handled separately in reducers for state change, and effects for handling side-effects. NGXS combines that together inside the “State” class. Which is a better method in my opinion. Also, the way NGXS utilizes TypeScript decorators really cleans up the code base and makes it feel like Angular. There are several other differences as well but these are the main ones.

One way to think about refactoring is to think about the how you would start something from scratch. Ask yourself, “If I did not already have NGRX in this application, what would I do at this step?”. That will always keep you on track during the refactoring process. For example, the best place to start when adding a new library in Angular is to add it to your module imports array!

Lastly, before we really get into the refactoring process there’s something very important to know. You can run both NGRX and NGXS at the same time! I repeat, do NOT feel like this has to be an all or nothing refactor. I will show you how to run them both in parallel and let you refactor things incrementally.

You can run both NGRX and NGXS at the same time!


Step 1: Installation

Before you can start refactoring you must install all the dependencies you need. I use NPM so that’s what I will show here but you can use Yarn if you like.

If you’re using Angular 6.x or higher run:

npm install @ngxs/store — save

This will get you the basics. However, you will probably want more depending on your specific needs. You will need the devtools plugin though. Run this to install:

npm install @ngxs/devtools-plugin — save-dev

If you need more plugins you can find them here.

Lastly, you will want to add the CLI schematics to help with generating the new files by running:

ng add @ngxs/schematics

Step 2: Importing the modules

This step will once again change depending on your specific needs but assuming you have global app-level state, you can import the default modules inside your AppModule file.

// app.module.tsimport { BrowserModule } from '@angular/platform-browser';// other imports here... (ngrx etc)import { NgModule } from '@angular/core';
import { NgxsModule } from '@ngxs/store';
import { NgxsReduxDevtoolsPluginModule } from '@ngxs/devtools-plugin';
import { YourState } from '../path/to/your-state';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
// ... other imports here (NGRX etc.) NgxsModule.forRoot([
YourState
]),
NgxsReduxDevtoolsPluginModule.forRoot({
name: 'NGXS store',
disabled: environment.production
})
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

Notice the Devtools plugin meta-data we added here. This will be used to differentiate the new NGXS store from the old NGRX store when using the inspector.

Step 3: Creating your State class

This step is where the largest changes will take place. In NGRX this code will be split into multiple different files. However, in NGXS you could potentially have it all in just one, though I don’t suggest it. So, let’s say you have the following files:

  • app.reducer.ts
  • app.effects.ts
  • app.actions.ts

Those files will become

  • app.state.ts
  • app.actions.ts

So right away we can see the boilerplate code melting away before our eyes! You could argue that you’re ignoring a separation of concerns by doing this but that’s all a matter of opinion. Let’s keep going.

Now let’s create the main files by running:

ng g @ngxs/schematics:store --name app

// app.state.tsimport { State, Action, StateContext } from '@ngxs/store';
import { User } from './models/user';
import { Login } from './app.actions.ts';
export interface AppStateModel {
user: User | undefined;
}
const initialState<AppStateModel> = { user: undefined };@State<AppStateModel>({
name: 'app',
defaults: initialState
})
export class AppState {
@Action(Login)
login({patchState}: StateContext<AppStateModel>, action: Login){
patchState({user: action.user});
}
}

Step 4: Creating your Actions file

This part is actually pretty straight forward. The actions are almost exactly the same in each library except for a few slight differences.

// app.actions.ts (NGRX)import { Action } from '@ngrx/store';export enum ActionTypes {
Login = '[Login Page] Login',
}
export class Login implements Action {
readonly type = ActionTypes.Login;
constructor(public payload:{ username:string; password:string}){}
}
export type Union = Login;

This file would become:

// app.actions.ts (NGXS)export class Login {
static readonly type = '[Login Page] Login';
constructor(public user: User){}
}

Much less code!

Now you just have to repeat that same process until all of your actions have been converted. Or until the feature you’re converting has been finished.

Step 5: Making improvements

This is a great start but we can do better. If you aren’t familiar with the concept of “facades” in Angular, this would be a great time to start learning. Essentially, it helps you keep your components powerless. They don’t get direct access to the store. They don’t get to mutate anything directly. It is a middle layer that handles all the business logic which makes it much easier to test as well. In this situation the largest value will come in the form of updating dependencies.

For example, if you have 100 components in your app that import the @ngrx/store to dispatch actions and select state you now have to update ALL of those import statements to use @ngxs/store. Which doesn’t sound that hard with the use of tools like “find and replace all” but it’s not that simple if you need to do this refactor incrementally over time. Keep in mind you will probably need to run both NGRX and NGXS at the same time. Meaning you will need to double your imports.

import { Store, select } from '@ngrx/store';

import { Store, Select } from '@ngxs/store;

If you look closely at these imports you’ll see that there is already an issue. We are duplicating imports with the same name. Store !== Store in this case. Meaning you will get a warning about duplicates. This is why I highly recommend abstracting this import into a singleton service that handles all the Store interactions for you, AKA a facade.

If we pull this into the facade service, app.service.ts we can just import it one time and then just inject the service into all the components!

// app.service.tsimport { Injectable } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { AppState } from './app.state';
@Injectable({
providedIn: 'root'
})
export class AppService {
// TODO: Add selectors and action dispatchers}

This is MUCH easier than doing something like this.

// app.component.ts  (don't do this)import { Store } from '@ngrx/store';
import { Store as ngxsStore } from '@ngxs/store';

This kind of aliasing gets very confusing, very fast. Not to mention you’re creating more tech debt from day one.

So if you plan on using both libraries and refactoring piece by piece, do your self a favor and use a facade.

Step 6: Replace Action handling in existing components

By now you must be asking yourself how the application will know which store to use. You have converted the Store but not the dispatching and selecting! This is how we will make the switch.

Right now, all your actions are still being dispatched by the NGRX store. In order to migrate it over we need to replace all the places where actions are getting dispatched with a wrapper function around the new facade implementation. For example:

// app.component.tsimport { Store } from '@ngrx/store';...component stuffthis.store.dispatch(
new Login(newUser)
);

will become…

// app.component.tsimport { AppService } from './app.service';...component stufflogin(newUser) {
this.appService.login(newUser);
}

Now the old NGRX store will have nothing to do with the actions that we have migrated over. Keep in mind there may still be some actions in your component that have not been converted yet, and that’s okay. Just do what is right for your current scope of migration.

Lastly, we can replace all the selectors. This is actually my favorite part of NGXS. I feel that it makes it very easy to select state from the store.

// app.component.ts (NGRX)...component stuffpublic user: User;this.user = this.store.pipe(select(fromRoot.getUser));

could become…

// app.service.ts import { Store, Select } from '@ngxs/store';
import { AppState, User } from './app.state.ts';
@Select(AppState) user$: Observable<User>;// app.component.ts (NGXS)...component stuffpublic user: User;ngOnInit() {
this.appService.user$.subscribe(user => this.user = user);
}

There are several different ways to do selectors. I encourage you to explore which way makes the most sense to you. However, the main thing to keep in mind is that you need to make sure you’re selecting from the NEW store and consuming it in the component. The easiest way to make sure of that is to use your facade.

Step 7: Inspect both stores in the Devtools

At this point we should be ready to see things in action. We can do this by opening Redux devtools.

Inside the app.module.ts file we told the Redux devtools which stores we would like to use. We even added a specific name for it name: 'NGXS Store'. We can now use that, along with our existing NGRX store, in the devtools inspector.

After opening the devtools you should see a drop-down at the top right. Inside the drop-down list you will see all the stores currently available. You should see both stores at this point. There are two main steps you need to take to verify everything is working as intended.

1: Verify the old store is no longer tied to those actions.

2: Verify that the new store is updating when actions are dispatched.

You can do this by running through the application and causing events that will trigger actions you converted. Then make sure that it only updates state in your NGXS store.

Summary

I have shown you the basics of converting your app from NGRX to NGXS. The scale of this will change drastically depending on the size of your application. However, you must keep in mind that this does not have to be done all at once. I recommend breaking things down feature by feature. This way you you can closely follow the structure of your state and keep track of your progress. Also, don’t forget to remove state in your NGRX store as they get moved over to NGXS.

If you have any questions or tips to improve this guide please let me know.

How do you eat an elephant? One bite at a time! 😃

Josh Hicks

Written by

Software engineer, writer, traveler, weight lifter. Find more from me at www.hirejoshhicks.com

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade