In this tutorial, we'll build a simple store where items can be added and removed from cart, and we’ll manage the application’s state using ngrx/store. As we’ll see, it is easier to manage data flow in the application when side effects and data flow are abstracted from components.
Managing an application is tasking, as the application grows to a never ending maze that requires a makeshift map to navigate. When applications grow to be that complex, managing data throughout the application becomes a major headache. This is where the importance of state management libraries like Redux, MobX and ngrx/store arises.
An important advantage of state management libraries in large-scale applications, especially hierarchical ones, is the ability to abstract the state of the application from components into an application-wide state. This way, data can be passed around with ease and components can act independently of each other.
For Angular, a great state management library is ngrx/store. This is an RxJS-powered state management library. It uses a similar syntax to Redux: actions, reducers, stores, effects, and RxJS’s reactive API.
In this tutorial, we’ll be building a fruit store using Angular. In our small store, a user will be able to add and remove fruits from the cart. We’ll also look at how we can use Effects for handling network requests, reducers and actions for data management. We’ll be setting up a minimal server using Express that will serve products to the Angular application.
To follow this tutorial, a basic understanding of Angular and Node.js is required. Please ensure that you have Node and npm installed before you begin.
If you have no prior knowledge of Angular, kindly follow the tutorial here. Come back and finish this tutorial when you’re done.
We’ll be using these tools to build our application:
Here’s a screenshot of the final product:
Initializing Application and Installing Dependencies
To get started, we will use the CLI (Command Line Interface) provided by the Angular team to initialize our project.
First, install the CLI by running npm install -g @angular/cli
. npm is a package manager used for installing packages. It will be available on your PC if you have Node installed; if not, download Node here.
To create a new Angular project using the CLI, open a terminal and run:ng new fruit-store --style=scss
This command is used to initialize a new Angular project; the project will be using SCSS as the pre-processor.
Next, run the following command in the root folder of the project to install dependencies.
// install depencies required to build the server
npminstall express body-parser
// front-end dependencies
npminstall @ngrx/store @ngrx/effects
Start the Angular development server by running ng serve
in a terminal in the root folder of your project.
Building Our Server
We’ll build our server using Express. Express is a fast, unopinionated, minimalist web framework for Node.js.
Create a file called server.js
in the root of the project and update it with the code snippet below
// server.jsconst express =require('express');const bodyParser =require('body-parser');const app =express();const port = process.env.PORT ||4000;const fruits =require('./fruits');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended:false}));
app.use((req, res, next)=>{
res.header('Access-Control-Allow-Origin','*');
res.header('Access-Control-Allow-Headers','Origin, X-Requested-With, Content-Type, Accept');next();});
app.get('/fruits',(req, res)=>{
res.json(fruits);});
app.listen(port,()=>{
console.log(`Server started on port ${port}`);});
The calls to our endpoint will be coming in from a different origin. Therefore, we need to make sure we include the CORS headers (Access-Control-Allow-Origin
). If you are unfamiliar with the concept of CORS headers, you can find more information here.
This is a standard Node application configuration, nothing specific to our app.
We’re creating a server to feed data to our application so we can see how Effects can be used to fetch external resources to populate the store.
Create a file named fruits.js
that will hold the products for our store. Open the file and populate it with the code below:
//fruits.js
module.exports =[{"name":"Berries","price":23.54,"image":"/assets/images/berries.jpeg","description":"The bestest fruit known to man. Sweet yet sour but beautiful"},{"name":"Orange","price":10.33,"image":"/assets/images/oranges.jpeg","description":"Succulent and watery, you'll never run out of water"},{"name":"Lemons","price":12.13,"image":"/assets/images/lemons.jpeg","description":"Sour but important for revitalization"},{"name":"Bananas","price":10.33,"image":"/assets/images/banana.jpeg","description":"An every day fruit, can be served with every dish"},{"name":"Apples","price":10.33,"image":"/assets/images/apple-item.png","description":"Sliced and served with your salad. Served as snacks midway through the day"},{"name":"Sharifa","price":10.33,"image":"/assets/images/unknown.jpeg","description":"A great fruit, also known as custard apple"}]
Note: All image assets can be found in the GitHub repository here. Images were gotten from https://pexels.com.
Start the server by running the following command in a terminal within the project folder:
node server.js
Home View
To get started, we’ll define the views for the application, starting from the home page. The home page will house the products grid and the header. Using the CLI, we’ll create a component named home
within the src/app
folder. Run the command below in the project folder to create the home
component:
ng generate component home
Open the home.component.html
file and replace it with the content below.
<!-- /src/app/home/home.component.html --><main><sectionclass="banners"><div><imgsrc="/assets/images/fruits.jpeg"alt="Apples in a bunch"/></div><div><imgsrc="/assets/images/berry.jpeg"alt="A bunch of berries"/></div><div><imgsrc="/assets/images/banner-1.jpeg"alt="Slushy oranges"/></div><div><imgsrc="/assets/images/banner-2.jpeg"alt="A diverse set of fruits"/></div></section><sectionclass="product-area"><!-- TODO: Create product list component --></section></main>
You can find image assets used here.
In the snippet above, we’ve defined an area for the banners and products list. The banner area will house four banner images. We’ll go about creating the product list component later in the tutorial.
Styling the Home Component
Next, we’ll go about styling the home page, the banner area to be exact. We’ll give the images a defined height and give the container a max width.
// src/app/home/home.component.scssmain{width:90%;margin: auto;padding:20px 15px;.banners{display: flex;align-items: center;justify-content: center;div{width:26%;margin-right:10px;img{height:200px;width:100%;max-width:100%;border-radius:10px;object-fit: cover;}}}}
Since we’ll be using external fonts, we’ll update the src/index.html
file with a link
tag alongside the src/styles.scss
file.
<!-- index.html --><!doctype html><htmllang="en"><head><metacharset="utf-8"><title>MyStore</title><basehref="/"><metaname="viewport"content="width=device-width, initial-scale=1"><linkhref="https://fonts.googleapis.com/css?family=Dosis:400,500,700|Lobster"rel="stylesheet"><linkrel="icon"type="image/x-icon"href="favicon.ico"></head><body><app-root></app-root></body></html>
Then we’ll select Dosis as our default font family. We’ll also negate the default padding
and margin
on the body
and html
elements. Open the styles.scss
file and update it with the following content:
// styles.scss/* You can add global styles to this file, and also import other style files */body, html{margin:0;padding:0;font-family:'Dosis', sans-serif;background-color: whitesmoke;}
Header Component
The header component will display the the application logo and the cart total. The component will be subscribed to the store listening for changes to the cart array. More light on this when the NgRx/store library is introduced later in the article.
Run the following command to create the header component:
ng generate component header
Next, open the src/app/header/header.component.html
file and update it to look like the code below:
<!-- src/app/header/header.component.html --><header><divclass="brand"><imgsrc="/assets/images/logo.png"alt="avatar"/><h5>The Store</h5></div><divclass="nav"><ul><li><imgsrc="/assets/images/shopping-bag.png"alt="cart"/><spanclass="badge"*ngIf="cart.length > 0">{{ cart.length }}</span></li></ul></div></header>
Note: Any image asset used can be found here in the GitHub repository.
Next, we’ll style the header. Open the header.component.scss
file and update it with the snippet below:
//header.component.scssheader {display: flex;background-color: white;margin:0;padding:5px 5%;color: whitesmoke;box-shadow:02px 4px 0rgba(0, 0, 0, 0.1);.brand {flex:1;display: flex;align-items: center;img {height:35px;border-radius:50%;margin-right:17px;}h5 {font-family:'Lobster', cursive;font-size:23px;margin:0;letter-spacing:1px;color:rgb(52, 186, 219);background:linear-gradient(90deg,
rgba(52, 186, 219, 0.9878326330532213)44%,
rgba(0, 255, 190, 1)100%);-webkit-background-clip: text;-webkit-text-fill-color: transparent;}}ul {list-style: none;padding-left:0;display: flex;li {display: flex;align-items: center;position: relative;img {width:40px;}.badge {height:20px;width:20px;font-size:11px;color: white;background-color:#35badb;display: flex;justify-content: center;align-items: center;position: absolute;top:0;right: -10px;border-radius:50%;}}}}
Open up the header.component.ts
file and declare the cart
variable used in the HTML file.
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss']
})
export class HeaderComponent implements OnInit {
constructor() {
}
cart = [];
ngOnInit() {}
}
App Component
After creating the home
and header
components, the next step is to render the components in the root App
component. Open the app.component.html
file within the src/app/
directory. Update it to render both Header
and Home
components.
<!-- app.component.html --><div><app-header></app-header><app-home></app-home></div>
Start the application server by running the following command: npm start
or ng serve
.
Then navigate to http://localhost:4200 on your browser. You should see the something similar to the screenshot below:
Make sure to get the image assets from GitHub or use your preferred images.
Introducing NgRx/store
NgRx/store is a library for managing state in your Angular applications, it is a reactive state management library powered by RxJS. Similar to Redux, this library can be used to manage the flow of data throughout your application, when actions are dispatched, reducers act on them and mutate the store. Another library we’ll be working with is NgRx/effects. Effects are commonly used to handle side effects in your application, like fetching data from an external resource.
The first step is to create and assign actions. The actions will be mapped to constants using an enum
. Create a folder named store
within the src/app
directory, this folder will hold everything relating to our application’s state management.
Within the store
folder, create a file called actions.ts
. Open the file and update it with the code below:
// src/app/store/actions.tsimport{ Action }from'@ngrx/store';interfaceProduct{
name:string;
price:number;
description:string;
image:string;}exportenum ActionTypes {
Add ='[Product] Add to cart',
Remove ='[Product] Remove from cart',
LoadItems ='[Products] Load items from server',
LoadSuccess ='[Products] Load success'}exportclassAddToCartimplementsAction{
readonly type= ActionTypes.Add;constructor(public payload: Product){}}exportclassGetItemsimplementsAction{
readonly type= ActionTypes.LoadItems;}exportclassRemoveFromCartimplementsAction{
readonly type= ActionTypes.Remove;constructor(public payload: Product){}}exportclassLoadItemsimplementsAction{
readonly type= ActionTypes.LoadSuccess;constructor(public payload: Product[]){}}exporttype ActionsUnion = AddToCart | RemoveFromCart | LoadItems | GetItems;
First, we declare an interface that defines the properties of the Product
object. Then we go on to declare unique actions to be used.
Actions are typically used to describe events in the application. When an event is triggered, a corresponding event is dispatched to handle the triggered events. An action is made up of a simple interface with a single property type
, the type
property is a unique identifier for the action.
An action type
is commonly defined using the following pattern [Source] event
— the source
where the event originates and the event description.
You can create actions using as an interface
or a class
. Classes are easier to use if you need to extend the action with a payload
property, so that’s what we did.
After creating actions, a type ActionsUnion
is exported. This export helps define all Actions in this feature area; it exposes the type information of the actions exported. You can read more on creating actions union here.
After creating actions, the next step is to create a reducer that handles transitions of state from the initial to the next based on the action dispatched. Create a file named reducer.ts
in the src/app/store
directory. Open the file and update it with the code below:
// src/app/store/reducer.ts
import { ActionsUnion, ActionTypes } from './actions';
export const initialState = {
items: [],
cart: []
};
export function ShopReducer(state = initialState, action: ActionsUnion) {
switch (action.type) {
case ActionTypes.LoadSuccess:
return {
...state,
items: [...action.payload]
};
case ActionTypes.Add:
return {
...state,
cart: [...state.cart, action.payload]
};
case ActionTypes.Remove:
return {
...state,
cart: [...state.cart.filter(item => item.name !== action.payload.name)]
};
default:
return state;
}
}
A reducer is simple pure function that transitions your application’s state from one state to the next. A reducer doesn’t handle side effects — it is a pure function because it returns an expected output for a given input.
First, we have to define the initial state of the application. Our application will display a list of items
and also allow user add and remove items from the cart
. So the initialState
of our application will feature an empty array of items
and an empty cart
array.
Next, we’ll define the reducer which is a function featuring a switch statement that acts on the type
of action dispatched.
- The first action type is the
LoadSuccess
action, which is called when products are successfully loaded from the server. When that happens, the items array is populated with that response. - The next action type is
Add
. This action is dispatched when a user wishes to add an item to cart. The action features apayload
property containing details of the item. The reducer takes the item and appends it to the cart array and returns the state. - The final case is the
Remove
action. This is an event telling the reducer to remove an item from cart. The cart is filtered using thename
of the item dispatched, and the item is left out of the next state.
You’re probably thinking that the numbers don’t add up. We created four actions but we’re only acting on three of them. Well, actions can also be used for effects network requests; in our case, fetching items from the server. We’ll look at creating a service to handle fetching the products from the server.
Registering the Reducer
After creating a reducer, it needs to registered in the StoreModule
. Open the app.module.ts
file and import the StoreModule
from the ngrx/store library as well as the ShopReducer
we just created.
//app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import {HttpClientModule} from '@angular/common/http';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { HeaderComponent } from './header/header.component';
import { ShopReducer } from './store/reducer';
@NgModule({
declarations: [
AppComponent,
HomeComponent,
HeaderComponent,
],
imports: [
BrowserModule,
HttpClientModule,
StoreModule.forRoot({ shop: ShopReducer }),
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
When registering the ShopReducer
, we assign it a unique identifier (shop
). This is useful in case you need to register multiple reducers. This need will arise in a larger application where several reducers are created to handle different areas of the application.
Fetching Products from the Server
To handle fetching products from the server, we’ll make use of the ngrx/effects library. The library can be used interact with services, abstracting them from components. Effects are used in collaboration with actions and reducers to transition state with the data returned after a network request.
First, we’ll create a service that will handle fetching items from the server. To create this service using the CLI, run the command below:
ng generate service fruits
Then open the file and update the content to be similar to the snippet below:
// src/app/fruits.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class FruitsService {
constructor(private http: HttpClient) {}
getAll() {
return this.http.get('http://localhost:4000/fruits');
}
}
Import the HttpClient
, create a method called getAll
and return a call to the server to get fruits using the HttpClient. Next, we’ll create an effects file that will make the network request using the FruitService
when the appropriate action is triggered.
Create a file named effects.ts
within the src/app/store
directory. Open the file and copy the following code into the file:
import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { EMPTY } from 'rxjs';
import { catchError, map, mergeMap } from 'rxjs/operators';
import { ActionTypes } from './actions';
import { FruitsService } from '../fruits.service';
@Injectable()
export class ShopEffects {
constructor(
private actions$: Actions,
private fruitsService: FruitsService
) {}
@Effect()
loadFruits$ = this.actions$.pipe(
ofType(ActionTypes.LoadItems),
mergeMap(() =>
this.fruitsService.getAll().pipe(
map(fruits => {
return { type: ActionTypes.LoadSuccess, payload: fruits };
}),
catchError(() => EMPTY)
)
)
);
}
An effect is simple a service with a @Effect
decorator. There’s a bit going on here so we’ll explain each strange keyword used here.
Actions
is an observable stream of all the actions dispatched after the application’s state has been reduced.- From the actions dispatched, we use the
ofType
operator provided by the library to filter the actions with the provided type (LoadItems
in our case). One or more action types can be provided to the pipeable stream. - The
mergeMap
operator by RxJS is for flattening and merging the actions into an Observable. - The
getAll
method of theFruitService
returns an observable that is mapped, and the response is then dispatched as an action, provided there was no error. - The
catchError
operator handles any errors encountered during the process.
After creating effects, we have to register it in the root module. Open the app.module.ts
file and update it to fit the snippet below:
import { BrowserModule } from '@angular/platform-browser';
...
import { EffectsModule } from '@ngrx/effects';
import { ShopEffects } from './store/effects';
@NgModule({
declarations: [
...
],
imports: [
...
EffectsModule.forRoot([ShopEffects])
],
...
})
export class AppModule {}
In the EffectsModule
, we can register our effects ShopEffects
. Multiple effects can be registered by adding the effects to the array.
Now that we’ve created actions to handle events in our application and reducers to transition state, let’s populate the store with items from the server using the effects. Before we do that, let’s define views for the product and products list.
Products List View
Run the following commands to generate components for the product item and product list:
ng generate component product
And for the product list run:
ng generate component product-list
Open the product.component.html
file in the src/app/product
directory and update with the code below:
// src/app/product/product.component.html
<divclass="product"><divclass="product-image-holder"><img[src]="product.image"[alt]="product.name"class="product-image"/></div><divclass="product-details"><pclass="product-details__name">{{ product.name }}</p><pclass="product-details__price">${{ product.price }}</p></div><divclass="product-description"><p>{{ product.description }}</p></div><divclass="product-actions"><buttonclass="product-actions__add"(click)="addToCart(product)"*ngIf="!inCart"><imgsrc="/assets/images/add-to-cart.png"alt="add to cart"/></button><buttonclass="product-actions__remove"(click)="removeFromCart(product)"*ngIf="inCart"><imgsrc="/assets/images/remove-from-cart.png"alt="remove from cart"/></button></div></div>
Here we have two buttons for adding to and removing an item from the cart. A flag inCart
is used to determine which of the button to display.
Note: All image assets can be found in the GitHub repository here.
Let’s style the component by updating the product.component.scss
file with the styles below:
// product.component.scss%button{border-radius:50%;display: flex;justify-content: center;align-items: center;height:32px;width:32px;cursor: pointer;&:hover {transform:scale(1.1);}img {width:16px;height:16px;}}.product {box-shadow:01px 1px 0rgba(0, 0, 0, 0.2);border-radius:5px;margin:015px 30px 0;width:286px;max-height:400px;height:320px;&:hover {transform:scale(1.05);border:1px solid #35BADB;.product-actions {display: flex;}}&-image {max-width:100%;width:300px;border-top-right-radius:5px;border-top-left-radius:5px;height:180px;object-fit: cover;}&-details {display: flex;justify-content: space-between;padding:8px 15px;&__price {font-weight:500;opacity:0.7;letter-spacing:1px;margin:0;}&__name {opacity:0.8;font-weight:500;margin:0;}}&-description {padding:10px 15px;p {opacity:0.6;margin:0;}}&-actions {display: none;justify-content: flex-end;padding:015px;&__add {@extend%button;border:2px solid rgb(52, 186, 219);}&__remove {@extend%button;border:2px solid indianred;}}}
Open the product.component.ts
file and update it with the variables and methods used in the HTML file.
// src/app/product/product.component.ts
import { Component, Input, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AddToCart, RemoveFromCart } from '../store/actions';
export interface Product {
name: string;
price: number;
description: string;
image: string;
}
@Component({
selector: 'app-product',
templateUrl: './product.component.html',
styleUrls: ['./product.component.scss']
})
export class ProductComponent implements OnInit {
constructor(private store: Store<{ items: []; cart: [] }>) {}
inCart = false;
@Input() product: Product;
addToCart(item: Product) {
this.store.dispatch(new AddToCart(item));
this.inCart = true;
}
removeFromCart(item: Product) {
this.store.dispatch(new RemoveFromCart(item));
this.inCart = false;
}
ngOnInit() {}
}
First we import the Store
observable from the ngrx/store
library. The store property will be used to dispatch actions.
The addToCart
method takes one parameter (item
); the method dispatches an action to add an item to cart. After dispatching the action, the inCart
property is set to true
. This flag is for identifying which items are in cart.
Meanwhile, the removeFromCart
method dispatches an action to remove an item from cart and updates the inCart
property to false
.
Next we’ll render the Product
component in the product-list
component. Open the product-list.component.html
file and render the Product
similar to the snippet below:
<!-- product-list.component.html --><divclass="product-list"><app-product*ngFor="let fruit of fruits"[product]="fruit"></app-product></div>
We’ll add some styles to the component’s stylesheet. Open the product-list.component.scss
file and add the styles below:
.product-list {padding:10px 0;margin-top:30px;display: flex;flex-wrap: wrap;}
The product list component will receive an Input
from the Home
component, so let’s update the component to take an Input
an array of fruits
. Update the product-list.component.ts
file to be similar to the snippet below:
import { Component, Input, OnInit } from '@angular/core';
import { Product } from '../product/product.component';
@Component({
selector: 'app-product-list',
templateUrl: './product-list.component.html',
styleUrls: ['./product-list.component.scss']
})
export class ProductListComponent implements OnInit {
constructor() {}
@Input() fruits: Product[] = [];
ngOnInit() {}
}
After making this change, the final step is to render the product list component in the home.component.html
file and dispatch an action to load the products from the server in the OnInit
lifecycle of the component.
Open the home.component.html
file and render the product list component within the element with the product-area
class attribute:
<main><sectionclass="banners">
...
</section><sectionclass="product-area"><app-product-list[fruits]="items"></app-product-list></section></main>
Then update the home component and make it similar to the snippet below:
import { Component, OnInit } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { GetItems } from '../store/actions';
import { Product } from '../product/product.component';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
constructor(private store: Store<{ items: Product[]; cart: [] }>) {
store.pipe(select('shop')).subscribe(data => (this.items = data.items));
}
items: Product[] = [];
ngOnInit() {
this.store.dispatch(new GetItems());
}
}
First we dispatch a new action GetItems
. The action type was registered in the effect that handled fetching products from the server. After dispatching the action, we use the Store
observable and the select
operator to select and subscribe to the store we registered in the AppModule
file.
When subscribed to the store, the data returned is the current state of our store. If you remember, the initial state of our store had two properties, both of which are arrays. In the home component, we need the array of items in the store, so using dot notation we’ll get the current items from the state.
After this change, if you visit http://localhost:4200, you should see all the latest changes we’ve made, including the ability to add and remove an item from cart.
If you try adding an item to the cart, you’ll notice it is successful, but our cart doesn’t update with the number of items in the cart. Well, this is because we’re not subscribed to the store, so we won’t get the latest updates on the cart.
To fix this, open the header.component.html
file and update the component to subscribe to the store in the component’s constructor.
import { Component, OnInit, Input } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { Product } from '../product/product.component';
@Component({
..
})
export class HeaderComponent implements OnInit {
constructor(private store: Store<{ items: []; cart: [] }>) {
store.pipe(select('shop')).subscribe(data => (this.cart = data.cart));
}
cart: Product[] = [];
ngOnInit() {}
}
Similar to the Home
component where we subscribed to the store and got the items
array from the state, here we’ll be subscribing to the cart
property of the state.
After this update, you should see the amount of items in cart when an item is added or removed from the cart.
Note: Ensure both that the Angular dev server is running on port 4200 and that the server is running on port 4000.
Conclusion
In this tutorial, we’ve built a simple store where items can be added and removed from cart. We’ve been able to manage the application’s state using NgRx/store. As we’ve seen, it is easier to manage data flow in the application when side effects and data flow are abstracted from components. The decision to pick a state management library is sometimes difficult. Some people introduce them too early, which adds another level of complexity to the application, and some people introduce them too late, but no matter what the case may be, state management libraries are helpful whenever they’re introduced.
I remember a popular quote about Flux:
You’ll know when you need Flux. If you aren’t sure if you need it, you don’t need it.
The same should be applied in this case. The choice rests on you. You can find the source code for this demo here.
“We are our choices.” - Jean-Paul Sartre
This blog has been brought to you by Kendo UI
Want to learn more about creating great web apps? It all starts out with Kendo UI - the complete UI component library that allows you to quickly build high-quality, responsive apps. It includes everything you need, from grids and charts to dropdowns and gauges.