Introduction to NgRx
Why
Angular projects are getting more and more complex these days. With handling all the user interaction, application state and accessing this state in every place, it might be necessary but quickly become over complicated. This is why having a one, global app-wide state management system that can be accessible throughout the application may be very useful in modern frontend application. One of the solutions available for Angular applications is NgRx.
What
In this post, I will give you a quick introduction on how to get started using NgRx in you Angular application. I'll base this intro on a simple Angular app where we can display a photo which then can be liked or disliked. You can find the entry point for this application on my GitHub repo. If you want to follow this article's code, please clone the repository and checkout entry_point
tag.
git clone git@github.com:ktrz/introduction-to-ngrx.git
git checkout entry_point
After cloning just install all the dependencies
yarn install
and you can see the example app by running
yarn start -o
We will convert this application to use the NgRx state management in just few steps.
How
Key concepts
Let's start with introducing a few key concepts and building blocks of NgRx. NgRx is a state management tool inspired by Redux, so the building blocks might be familiar to Redux users. To build and operate on our state, we will need the following basic building blocks:
- Store is where state of the application will be stored. It's both an Observable of a state and an Observer of action.
- Actions describe all the possible and unique events that can occur within the application. They can occur ie. by user interaction, communication with the server or can be a result of other actions.
- Reducers are what binds the actions and the state. All the state changes must occur as result of an action. Those changes are handled by pure functions called reducers. They take the current state and the latest action and compute new state value based on that.
- Selectors - to retrieve part of the state that we're interested in, we use pure functions which extract the portion of the state that a given component might be interested in.
NgRx flow
NgRx makes sure that all the interactions in the application follow a certain unidirectional flow. The following diagram illustrates the general flow of the state:
Getting started
The easiest way to add NgRx to the project and get started is to use a Angular CLI schematic.
sh
ng add @ngrx/store@latest
This command will
- add the
@ngrx/store
package topackage.json
->dependencies
-
yarn install
ornpm install
those dependencies - import the
StoreModule.forRoot({}, {})
in yourAppModule
Define Actions
The first thing to do after installing the necessary dependencies is to define some actions that can occur in our application. In our case, this can define the following action:
- like photo
- dislike photo
To put that into code, let's create the following file: src/app/store/photo.actions.ts
import {createAction} from '@ngrx/store';
export const likePhoto = createAction('[Photo List] Like Photo');
export const dislikePhoto = createAction('[Photo List] Dislike Photo');
In this file, we define all the actions (action creators) that can occur in our app. In general, the shape of the action has to be in the following share:
const someAction = {
type: 'Unique type',
/* ... some additional properties */
}
The required part is the type
property which will be used to identify our action. Other properties are optional and might be used to pass some additional data associated with the event ie. id of the liked/disliked photo.
What we've just defined are action creators. They are functions which return an action of a specific type. To create ie. like action we can do it as following:
const likeAction = likePhoto()
Define Reducer
Now that we have our actions defined, we need a way to consume them and thus make actual changes to the store's state. As described before, this is done using pure functions called reducers. We could define the functions from scratch using ie. switch case
statements, but NgRx comes with some handy helper functions which make this process much nicer. Let's create a file to keep our photo reducer in: src/app/store/photo.reducer.ts
import {createReducer, on} from '@ngrx/store';
import {dislikePhoto, likePhoto} from './photo.actions';
export type PhotoState = number;
const initialState: PhotoState = 0;
export const photoReducer = createReducer(
initialState,
on(likePhoto, state => state + 1),
on(dislikePhoto, state => state - 1)
);
As you can see, you import the actions which we've defined previously and use them in our reducer. createReducer
is an utility function which creates a reducer with a provided initial state. This gives us a nice quick way to define new reducers as well as great type inference. TypeScript knows the shape of state that photoReducer
operates on from the shape of initialState
object.
on(...)
function can be considered as case
statements within a switch
. We define that if we encounter a given action, we produce a new state derived from its previous value and optional properties provided within the action.
All this code could be rewritten in the following shape. In my opinion, the construction mentioned above is both more consise and self explanatory as well, so I suggest using it.
export function photoReducer(state = initialState, action: Action): PhotoState {
switch (action.type) {
case likePhoto.type:
return state + 1;
case dislikePhoto.type:
return state - 1;
default:
return state;
}
}
After creating a reducer, we need to let our application know that we want to use it. We can define it in our AppModule
file and add it to the properties of the StoreModule.forRoot
's first param.
imports: [
/* ... other modules */
StoreModule.forRoot({
photo: photoReducer
},
{}
)
],
Define Selectors
Now that we've defined both possible actions and reducer to handle them, we need a way to get the data from the store. To do this, we'll create another pure function called selectors. The responsibility of selectors is to transform the whole state object (which can we a really large object in real life examples) into small bits necessary for a specific part of the application. In our case, we need a way to get the photo information along with the number of likes/dislikes that it currently has.
Let's create another file in our src/app/store
directory src/app/store/photo.selectors.ts
import {createSelector} from '@ngrx/store';
import {PhotoState} from './photo.reducer';
const selectPhotoFeature = (state: { photo: PhotoState }) => state.photo;
export const selectPhoto = createSelector(selectPhotoFeature, likes => ({
title: 'Introduction to NgRx',
url: 'https://ngrx.io/assets/images/ngrx-badge.png',
likes
})
);
We are again using the utility function provided with NgRx - createSelector
. Because our state can be a nested tree of objects, this enables us to create more complex selectors based on the already existing ones. In this case, we combine selecting a photo
slice of our whole state with selection of the whole photo object (in this case most of its props are static). Whenever a number of likes change, we will get a new instance of the photo, which makes detection of changes super easy and performant.
Use all the building blocks in our component
Now that we've defined all the necessary building blocks, we can finally start using them in our AppComponent
. The first thing we need to do is inject the Store service into our component.
import {Store} from '@ngrx/store';
interface AppState {
photo: PhotoState;
}
@Component({/* ... */})
export class AppComponent {
constructor(private store: Store<AppState>) {
}
}
This gives us access to both selecting data from the store (as an Observable) and notifying it about actions happening (by dispatching actions).
Now let's select the state of the photo into a component's property and define methods for dispatching like and dislike actions.
import {select, Store} from '@ngrx/store';
import {selectPhoto} from './store/photo.selectors';
import {dislikePhoto, likePhoto} from './store/photo.actions';
@Component({/* ... */})
export class AppComponent {
photo$ = this.store.pipe(select(selectPhoto));
constructor(private store: Store<AppState>) {
}
onLike(): void {
this.store.dispatch(likePhoto());
}
onDislike(): void {
this.store.dispatch(dislikePhoto());
}
}
To select data from store, we can basically treat it as an Observable and use pipe
operator on it. To select data with our previously created selectors, we can use select
operator provided by @ngrx/store
package.
To dispatch actions and notify the store of it, we can use dispatch()
method and feed it with the necessary action (also defined above).
Now all that is left to do is hook up all this state and methods into our component's template.
@Component({
selector: 'app-root',
template: `
<div class="photos">
<app-photo class="photo" [photo]="photo$ | async" (like)="onLike()" (dislike)="onDislike()"></app-photo>
</div>
`,
})
As you can see, we use async
pipe to unwrap the photo$
observable and pass a plain object into the [photo]
input. The app-photo
component provides two outputs: like
and dislike
which we can react to with our AppComponent's methods.
For clarity purposes, let's define our PhotoComponent
in src/app/photo/photo.component.ts
(It's already defined within the entry point repo but needs some minor tweaks.)
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {Photo} from './photo';
@Component({
selector: 'app-photo',
template: `
<mat-card class="example-card" *ngIf="photo">
<mat-card-header>
<div mat-card-avatar class="example-header-image"></div>
<mat-card-title>{{photo.title}}</mat-card-title>
</mat-card-header>
<mat-card-content>
<img mat-card-image [src]="photo.url" alt="Intro to NgRx">
</mat-card-content>
<mat-card-actions align="end">
<button mat-icon-button color="warn" (click)="onLike()">
<ng-container *ngIf="photo.likes > 0; else notLikedButton">
<mat-icon [matBadge]="photo.likes" matBadgeColor="warn" color="primary">thumb_up</mat-icon>
</ng-container>
<ng-template #notLikedButton>
<mat-icon color="primary">thumb_up_off_alt</mat-icon>
</ng-template>
</button>
<button mat-icon-button (click)="onDislike()">
<ng-container *ngIf="photo.likes < 0; else notDislikedButton">
<mat-icon [matBadge]="-photo.likes" matBadgeColor="warn" color="primary">thumb_down</mat-icon>
</ng-container>
<ng-template #notDislikedButton>
<mat-icon color="primary">thumb_down_off_alt</mat-icon>
</ng-template>
</button>
</mat-card-actions>
</mat-card>
`,
styleUrls: ['./photo.component.scss']
})
export class PhotoComponent {
@Input() photo?: Photo | null;
@Output() like = new EventEmitter();
@Output() dislike = new EventEmitter();
onLike(): void {
this.like.next();
}
onDislike(): void {
this.dislike.next();
}
}
and PhotoModule
in src/app/photo/photo.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import {MatBadgeModule} from '@angular/material/badge';
import {MatButtonModule} from '@angular/material/button';
import {MatCardModule} from '@angular/material/card';
import {MatIconModule} from '@angular/material/icon';
import { PhotoComponent } from './photo.component';
@NgModule({
declarations: [PhotoComponent],
imports: [
CommonModule,
MatCardModule,
MatButtonModule,
MatIconModule,
MatBadgeModule
],
exports: [PhotoComponent]
})
export class PhotoModule { }
Now start your application again and see the result it the browser. You can still like/dislike the photo and the state of the app is stored withing NgRx store.
Conclusion
As you can see, setting up this example required a bit more code to start with. But when the application grows, this additional code pays off as we get one central place where all the state changes are happening. We can see exactly what actions are being dispatched (ie. by using Redux Devtools) and therefore our application is more maintainable.
In case you have any questions you can always tweet or DM at me @ktrz. I'm always happy to help!
More articles are coming soon! Do not miss any of them and visit the This Dot Blog to be up-to-date.
This Dot Labs is a modern web consultancy focused on helping companies realize their digital transformation efforts. For expert architectural guidance, training, or consulting in React, Angular, Vue, Web Components, GraphQL, Node, Bazel, or Polymer, visit thisdotlabs.com.
This Dot Media is focused on creating an inclusive and educational web for all. We keep you up to date with advancements in the modern web through events, podcasts, and free content. To learn, visit thisdot.co.
Top comments (0)