Well, now that you have your web application ready, you start to feel that there is a way that you could possibly handle reactive programming issues in a better way. Well, the good news is that there is something just for that, and that is ngxs.
But, what is ngxs, you may wonder. Simply put, it is a library which makes it possible for your application to have a single source of truth in a predictable manner.
So, the first part of this tutorial will give an introduction to what ngxs is and the second part will now show the moving parts needed to use it when it comes to angular state management.
Finally, we will look at how angular ngxs can be added onto your angular application and how to use it to update your application.
Well, as mentioned above, it is a library for angular state management. Okay, that is correct, but what is it? Well, to understand what is it, it is best to understand what makes it tick, as in, what are the moving parts.
There are three core parts that define ngxs:
Well, I know if you have worked with other state management libraries like ngrx, you must have wondered why we have no selectors and reducers. Well, simply put, ngxs has less boiler plate than ngrx does.
So, with the core parts in place, the next thing is understand what each of them actually are.
State
Yeah, you guessed it right, this is what defines the current standing of your web application. It is this which contains the most recent updates available to your application, and thus keeps the user interface in sync as well.
Another IMPORTANT distinguishing thing is that with unlike ngrx where we have reducers separately used to create the next state, with angular ngxs, the next state is actually created within the state itself!
Well, this may be a lot to take in, but not to worry, we will see an example at the bottom.
Model
This is simply a definition of how your store above would look like.
Actions
Well, it seems that both in ngxs and ngrx, actions are the constant feature. Just as you would have guessed it, actions decide what needs to be done immediately next in the application.
Of course there may be multiple actions happening at a time, for example you have Websockets in place. Now to distinguish these actions, each action is defined in a way unique to it as you will see in the example provided.
We will use a hypothetical application which fetches artists from somewhere like spotify for example. Then our component will be updated purely by angular ngxs, so no more relying on rxjs observables and the likes.
Step 1
We will create a model file as well as an interfaces file. The interfaces will have definition of how our data attributes will look like. As for the model file, we will only have the model definition.
export interface IArtist {
image: string;
name: string;
genres: Array<string>;
albums: Array<string>;
uuid: string;
}
Defining our angular ngxs model:
import { IArtist } from "./interfaces";
export interface ArtistsStateModel {
searchResults: Array<IArtist>;
}
Step 2
We will create an actions file and add an action for fetching the artists.
export namespace ArtistsActions {
export class SearchForArtist {
static readonly type = '[Artists] Search For Artist';
constructor (
public searchTerm: string
) {}
}
}
So, to explain the above code:
export namespace ArtistsActions
Here, we are namespacing this actions file, and wrapping all our actions, so that we just import the name of the namespace as a unit, instead of importing the individual action. This reduces margin for error and makes code look a little bit cleaner.
static readonly type
This line will define the name for our action. You can think of it of how you would want your action to be identified by the application.
Well, this name can be anything you want, but it is best to have it like so:
'[Component calling the action] Name of the action'
as this will make it easier to troubleshoot.
The other important part is the:
constructor (
public searchTerm: string
) {}
Well, you guessed it right. This is the parameter to be passed into the action. In this case, we are passing in the searchTerm. But it is optional, if you don’t need this ngxs action to receive any parameters.
Step 3
Now, we define our angular ngxs state, like so:
import { Injectable } from "@angular/core";
import { Action, State, StateContext, StateToken } from "@ngxs/store";
import { ArtistsActions } from "./actions";
import { ArtistsStateModel } from "./model";
import { produce } from 'immer';
import { ApiService } from "../services/api.services";
import { tap } from "rxjs/operators";
const ARTIST_STATE_TOKEN = new StateToken<ArtistsStateModel>('artists');
@State({
name: ARTIST_STATE_TOKEN,
defaults: {
searchResults: []
}
})
@Injectable()
export class ArtistsState {
constructor(
private apiService: ApiService
) {}
@Action(ArtistsActions.SearchForArtist)
searchForArtist(ctx: StateContext<ArtistsStateModel>, action: ArtistsActions.SearchForArtist) {
return this.apiService.get('search', {q: action.searchTerm, type: 'artist'}).pipe(
tap(r => ctx.setState(produce(draft => {
draft.searchResults = r;
})))
)
}
}
Breaking this code line by line:
const ARTIST_STATE_TOKEN = new StateToken<ArtistsStateModel>('artists');
Here, we define how this piece of the whole application state will be identified in the whole angular state management. The type accepted by StateToken is the model definition for the state, in this case ArtistsStateModel, and the name is the string identity for the piece of state.
The next part is this:
@State({
name: ARTIST_STATE_TOKEN,
defaults: {
searchResults: []
}
})
What this does is simply set the name identified by our state token, as well as the initial state (passed in as defaults) which should be analogous to the model definition.
@Injectable()
Well, an angular ngxs state is a service of sorts. So that is why we define it using the @Injectable() decorator.
constructor(
private apiService: ApiService
) {}
Here, we inject the services which we will be using, and in our case, since we are making an external request, we add our ApiService.
@Action(ArtistsActions.SearchForArtist)
Remember when we said actions say what needs to be done, well, here it is. This @Action decorator will accept a parameter of the action which it needs to listen for and act upon when such an action is dispatched.
searchForArtist(ctx: StateContext<ArtistsStateModel>, action: ArtistsActions.SearchForArtist) {
return this.apiService.get('search', {q: action.searchTerm, type: 'artist'}).pipe(
tap(r => ctx.setState(produce(draft => {
draft.searchResults = r;
})))
)
}
So, the above method is the decorated class method, which accepts a context (the state model) and an action(the action which it has been decorated with).
Notice that we are tapping into it. This is because we need to invoke the actual fetching on the api.
action.searchTerm
Remember this code, we defined it in our action and now we will get it like so.
tap(r => ctx.setState(produce(draft => {
draft.searchResults = r;
})))
This is actually the most important bit, because it is what makes it possible to now update the state with the new results.
We have produce method imported from immerjs (this is out of the scope of this tutorial). What it does is that it makes it possible to update the draft version of the current state, and return it, so that the draft’s updated attributes are then mapped onto the state attributes which match it, thus update the state. The reason for this is because it is not advisable to update the ngxs state directly.
So, you can see that what tap gives us, we now set this to the attribute of our state that we want to update.
Step 4
Now, let’s define our component:
import { Component } from '@angular/core';
import { IArtist } from "./interfaces";
import { ArtistsActions } from './actions';
@Component({
moduleId: 'module.id',
selector: 'lib-artists',
templateUrl: 'artists.component.html',
styleUrls: [
'artists.component.scss'
]
})
export class ArtistsComponent {
private searchResults$: Observable<Array<ISearchItem>> = new Observable<Array<ISearchItem>>();
/**
*
*/
public getSearchResults$(): Observable<Array<ISearchItem>> {
return this.searchResults$;
}
constructor(
private store: Store
) {
this.searchResults$ = this.store.select(state => state.artists.searchResults || of([]))
}
/**
*
* @param searchTerm
*/
public emitSearchEvent(searchTerm: string): void {
this.store.dispatch(new ArtistsActions.SearchForArtist(searchTerm));
}
}
Most of the code above is simple angular, so we will focus on the parts related to angular state management.
private searchResults$: Observable<Array<ISearchItem>> = new Observable<Array<ISearchItem>>();
This part defines our observable of search results. The results will be a list of IArtists type.
/**
*
*/
public getSearchResults$(): Observable<Array<ISearchItem>> {
return this.searchResults$;
}
This is a getter method which we will use in our html.
constructor(
private store: Store
) {
this.searchResults$ = this.store.select(state => state.artists.searchResults || of([]));
}
We need to define the store so that we can use it’s methods. The first method is the .select method, and this will return an observable of the most recent search results. Notice that because the search results may be empty, we are using rxjs of operator to return an initial observable of empty array.
/**
*
* @param searchTerm
*/
public emitSearchEvent(searchTerm: string): void {
this.store.dispatch(new ArtistsActions.SearchForArtist(searchTerm));
}
This method will listen for when a user searches and then it will dispatch the action.
Step 5
Finally, in our search component html file, we will add this:
<div class="artist" *ngFor="let artist of (getSearchResults$()|async) || []"></div>
So, here, we are using an async pipe to read the latest emitted value from our getSearchResults$() method.
As you can see, it is this easy to use ngxs for your angular state management issues.
Well, that is it for now.