Migrating from NGRX to NGXS in Angular 6

How do you eat an elephant?

Disclaimer

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

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

// 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

  • 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

// 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

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

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

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

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! 😃

Software engineer, writer, traveler, weight lifter

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store