By Gary simon - Apr 10, 2018
Last week, I created a tutorial that showed users how to use Ngrx for state management in Angular.
This week, I'm going to take the same exact sample project and walk you through the process of doing the same thing, except with NGXS.
Ngxs is a different approach to Angular state management that offers a few benefits, namely less boilerplate code.
Let's get started!
Be sure to Subscribe to the Official Coursetro Youtube Channel for more videos.
As always, we're going to use the Angular CLI to start a new project in the console:
> ng new ngxs-intro
> cd ngxs-intro
Next, we'll install ngxs store, logger-plugin and devtools-plugin:
> yarn add @ngxs/store
> yarn add @ngxs/logger-plugin @ngxs/devtools-plugin --dev
I'm using yarn here, but you can use npm as well. The logger-plugin and devtools-plugin are there to help you when you're developing with NGXS. The logger plugin outputs the state in the console, and the devtools-plugin allows NGXS to work with Redux Devtools chrome extension.
While we're here in the console, we're going to use the Angular CLI to generate two components:
> ng g c read
> ng g c create
We'll use the read component to read from ngxs store and our create component to add data to the store.
Create the folder and file: /src/app/models/tutorial.model.ts with the contents:
export interface Tutorial {
name: string;
url: string;
}
Our tiny little app will allow users to save a name and url of a tutorial. Awesome, huh?
Similar to Ngrx, we define actions in NGXS as well. We'll create two actions for adding and removing tutorials.
Create the folder and file: /src/app/actions/tutorial.actions.ts with the contents:
import { Tutorial } from './../models/tutorial.model'
export class AddTutorial {
static readonly type = '[TUTORIAL] Add'
constructor(public payload: Tutorial) {}
}
export class RemoveTutorial {
static readonly type = '[TUTORIAL] Remove'
constructor(public payload: string) {}
}
The main difference here from Ngrx to Ngxs, is that in this example, the readonly type is static.
If our actions require data -- payload - we can pass it in through a constructor.
A key difference between Ngrx and Ngxs is how the state is handled. The state file(s) in Ngxs take the place of reducers in Ngrx. This is done by utilizing various decorators.
Create the folder and file: /src/app/state/tutorial.state.ts with the content:
// Section 1
import { State, Action, StateContext, Selector } from '@ngxs/store';
import { Tutorial } from './../models/tutorial.model'
import { AddTutorial, RemoveTutorial } from './../actions/tutorial.actions'
// Section 2
export class TutorialStateModel {
tutorials: Tutorial[];
}
// Section 3
@State<TutorialStateModel>({
name: 'tutorials',
defaults: {
tutorials: []
}
})
Add the following contents beneath the 3 sections above:
// Sections 1, 2 and 3 above removed for brevity
export class TutorialState {
// Section 4
@Selector()
static getTutorials(state: TutorialStateModel) {
return state.tutorials
}
// Section 5
@Action(AddTutorial)
add({getState, patchState }: StateContext<TutorialStateModel>, { payload }:AddTutorial) {
const state = getState();
patchState({
tutorials: [...state.tutorials, payload]
})
}
@Action(RemoveTutorial)
remove({getState, patchState }: StateContext<TutorialStateModel>, { payload }:RemoveTutorial) {
patchState({
tutorials: getState().tutorials.filter(a => a.name != payload)
})
}
}
Now with our NGXS code ready to go, we need to add it to our App's module file.
Open /src/app/app.module.ts and add the following:
// Other imports removed for brevity
import { NgxsModule } from '@ngxs/store';
import { TutorialState } from './state/tutorial.state';
import { NgxsReduxDevtoolsPluginModule } from '@ngxs/devtools-plugin';
import { NgxsLoggerPluginModule } from '@ngxs/logger-plugin';
@NgModule({
...
imports: [
BrowserModule,
NgxsModule.forRoot([
TutorialState
]),
NgxsReduxDevtoolsPluginModule.forRoot(),
NgxsLoggerPluginModule.forRoot()
]
...
})
export class AppModule { }
Easy!
Open up /src/app/create/create.component.ts and add the following:
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngxs/store';
import { AddTutorial } from './../actions/tutorial.actions'
@Component({
// props removed for brevity
})
export class CreateComponent implements OnInit {
constructor(private store: Store) { }
addTutorial(name, url) {
this.store.dispatch(new AddTutorial({name: name, url: url}))
}
ngOnInit() {
}
}
Similar to Ngrx, we use .dispatch() to dispatch an action. In our case, we're importing AddTutorial from our actions and dispatching it with a payload of name and url, which will come from a form.
Next, open create.component.ts and add:
<div class="left">
<input type="text" placeholder="name" #name>
<input type="text" placeholder="url" #url>
<button (click)="addTutorial(name.value,url.value)">Add a Tutorial</button>
</div>
Update our /src/app/app.component.html file:
<app-create></app-create>
<app-read></app-read>
Also, add some CSS is /src/styles.css:
body, html {
margin: 0;
padding: 0;
font-family: 'Arial';
}
.left, .right {
float:left;
width: calc(50% - 6em);
padding: 3em;
}
input[type="text"] {
width: 100%;
padding: 5px;
margin-bottom: 10px;
}
At this point, if you save and serve the project with ng serve -o in the console, you can add tutorials with the forum and view the logger in the console of the browser. It will show that the state is being updated.
We've written to the state, let's read from it now.
Open up /src/app/read/read.component.ts and add the following:
import { Component, OnInit } from '@angular/core';
import { Store, Select } from '@ngxs/store';
import { Tutorial } from './../models/tutorial.model'
import { TutorialState } from './../state/tutorial.state' // We will use this shortly
import { Observable } from 'rxjs/Observable'
import { RemoveTutorial } from './../actions/tutorial.actions'
@Component({}) // Removed for brevity
export class ReadComponent implements OnInit {
tutorials$: Observable<Tutorial>
constructor(private store: Store) {
this.tutorials$ = this.store.select(state => state.tutorials.tutorials)
}
delTutorial(name) {
this.store.dispatch(new RemoveTutorial(name))
}
ngOnInit() {}
}
Here, we're using an instance of Store to select the tutorials from the state and binding it to an observable.
We're also creating a method to handle dispatching the RemoveTutorial action.
Visit the create.component.html template and specify the following:
<div class="right" *ngIf="tutorials$">
<h3>Tutorials</h3>
<ul>
<li (click)="delTutorial(tutorial.name)" *ngFor="let tutorial of tutorials$ | async">
<a [href]="tutorial.url" target="_blank">{{ tutorial.name }}</a>
</li>
</ul>
</div>
If you save the project and add a tutorial, the tutorials will now show up in the read component's unordered list. If you try clicking to the right of each tutorial, it will remove the tutorial from the state.
If you visit the read.component.ts file again and make these adjustments, you will find that we can make use of our state's Selector function for returning the state in even less code:
//tutorials$: Observable<Tutorial>
@Select(TutorialState.getTutorials) tutorials$: Observable<Tutorial>
constructor(private store: Store) {
//this.tutorials$ = this.store.select(state => state.tutorials.tutorials)
}
This allows us to use the @Select decorator to pass in the getTutorials selector that we created in the state!
We just scratched the surface of NGXS here, but hopefully you now have a basic understanding of how to use this state management solution for Angular.