Thursday, November 23, 2017

Angular 2 and Redux simplified

copyright from: beautiful-angular

Redux: Buzzword for simple idea

Redux sounds like some new cool technology but in fact it is more of a convention in how our frontend apps consume data.
Main idea is to store every piece of data in centralized container and change it only using set of predefined actions (something like dispatching an event). This way data flows in only one direction and chances of breaking something are reduced.

Why Redux in Angular project?

I started using this approach while I was developing one huge HTML5 banner creation software and ad serving network. There were so many components and services inside that Angular project and bits of data were scattered all over the place.
I knew there has to be some better way to store data and soon I discovered ngrx/store project. From their GitHub repository:
RxJS powered state management for Angular applications, inspired by Redux
They also offer many free tools, I mostly use their DevTools for Google Chrome. It shows current state of store as well as detailed timeline of dispatched actions and store modifications.
Redux DevTools
From above image you can see how easy it is to look for certain changes or store variables, much easier than any other way of handling data in Angular.

Installation

I am using angular-cli for my projects and thanks to it I don’t have to think about SystemJS and loading 3rd party packages.
npm install @ngrx/core @ngrx/store --save
For DevTools package:
npm install @ngrx/store-devtools --save
I will import ngrx module into app.module.ts component and DevTools module:
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { StoreModule } from '@ngrx/store';
In imports array I’ll import these 2 modules:
imports: [
    //...
    StoreModule.provideStore({ 
      //place for future reducers
    }),
    StoreDevtoolsModule.instrumentOnlyWithExtension()
  ],
Everything is clear from above steps, only new term is reducer.

Reducers

In terms of ngrx/store package, reducers are list of possible actions (in this case pure functions) for each property defined in the store. I am coming from PHP/Laravel background, so most similar to reducers are Event Listeners or Command Handlers.
Usual actions for each resource are CREATE, UPDATE and DELETE.
This is one of my reducers:
export const campaigns = (state: any = [], {type, payload}) => {
    switch (type) {
        case 'ADD_CAMPAIGNS':
            return payload;
        case 'CREATE_CAMPAIGN': 
            return [...state, payload];
        case 'UPDATE_CAMPAIGN':
            return state.map(campaign => {
                return campaign.token === payload.token ? Object.assign({}, campaign, payload): campaign;
            });
        case 'DELETE_CAMPAIGN': 
            return state.filter(campaign => {
                return campaign.token !== payload.token;
            });
        default: 
            return state;
    }
}
I am storing my reducers in dedicated directory _reducers and this reducer above is named campaign.reducer.ts.
Angular needs to be aware of this new reducer so I’ll include it in app module:
import { campaigns } from './_reducers/campaign.reducer';
I will push this new reducer into StoreModule:
imports: [
    //...
    StoreModule.provideStore({ 
      campaigns
    }),
    StoreDevtoolsModule.instrumentOnlyWithExtension()
  ],

App Store

Reducers are way of changing data in the store, but we still need some way to read that data.
That is done using application store, centralized place where all of store properties are kept. In terms of TypeScript that is a simple interface with dedicated properties for each store and type of each property is actual model which is used.
I am storing app.store.ts file in root folder of my application, on same level as app.module.ts
import { Campaign } from './_models/campaign';
export interface AppStore {
    campaigns: Campaign[];
    // other properties...
}

Service for manipulating AppStore

I created simple convention for my Angular projects, it says:
Angular services communicate with AppStore in both ways. They dispatch actions and they read data from the store.
This way components only need to inject service instance and when something changes it is usually easily changed on only one place, the service.
import { Injectable } from '@angular/core';
import { Response, Http } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { Store } from '@ngrx/store';
import { Campaign } from '../_models/campaign';
import { BaseService } from '../_services/base.service';
import { HttpService } from '../core/http.service';
import { AppStore } from '../app.store';
@Injectable()
export class CampaignService extends BaseService {
    // Redux based variables
    campaigns: Observable>; 
constructor(
        private http: HttpService,
        private store: Store
        ) {
            super();
            this.campaigns = store.select( store => store.campaigns);
        }
//...
Focus your attention on the constructor of CampaignService class. You can see I am using DI to create AppStore instance, and in the definition of the constructor I am selecting campaign store and assigning it’s value to campaigns property.
Store returns observable, so we can subscribe in our components and listen for changes, which is a big plus.

Using the service

I will skip import part of the component and class definition, I’ll just show most important parts of CampaignComponent here:
// Redux based variables
campaigns: Observable>;
private subscription: Subscription;
constructor(
        private campaignService: CampaignService
        ) { 
            this.campaigns = campaignService.campaigns;
        }
ngOnInit() {
        this.subscription = this.campaigns
                    .subscribe(
                        campaigns => {
                            //Do something
                        }
                            ,
                        error => 
                            //Do something
                    );
        this.campaignService.loadCampaigns();
     }
ngOnDestroy() {
        this.subscription.unsubscribe();
    }
In the constructor of this component I am injecting CampaignService instance, after that I am creating link between CampaignService property campaigns and components property campaigns.
This allows me to directly use campaigns in component, I can print them using async pipe or I subscribe for changes as I did in this example.
In ngOnDestroy method I am removing subscription, this is important for preventing memory leaks, I won’t go into details, it is good to accept this practice as convention.

Loading data via REST

You may noticed call to loadCampaigns method in CampaignComponent, that method is actually contacting backend and loading data into Redux store.
Cause I am using observables when new data is loaded in the store, my components will immediately be updated with new content.
loadCampaigns()  {
        return this.http.get(this.campaignUrl)
                        .map((res: Response) => {
                            let body = res.json();
                            return body.data || {};
                        })
                        .map((payload: Campaign[] ) => {
                            return { type: 'ADD_CAMPAIGNS', payload };
                        })
                        .subscribe((action) => {
                            this.store.dispatch(action);
                        });
    }
In loadCampaigns I am using custom http class which extends angular/http class. That way I can pass authentication headers into every request and manage error/status handling on one place. I described my custom http service here.
Cause authentication is different topic let’s imagine that http is angular class and that this route is unprotected.
First map statement accepts backend response, encodes it in JSON and passes data property if it exists or empty object if not.
Second map statement takes content of data property and creates array of Campaign objects. After that it returns simple object with defined action type and payload.
Finally subscribe statement takes passed action and dispatches it to Redux reducer.
That’s all.

Redux in Angular equals simplicity

At first it may look like overkill, defining reducer for each resource, inserting that reducer into StoreModule, adding new property into AppStore, injecting AppStore into service, … returning observables from the store, but when you get used to it, it starts to make sense.
Let us imagine simple app.component.html file which only prints list of campaigns:
  • {{campaign.name}}
As soon as loadCampaigns method returns response, this list will be updated. And not only that list, any other service/component which uses same store will be updated.

Lazy loaded modules

When you create lazy loaded module or complicated routing module with many levels of lazy loaded modules it becomes too complicated to track which instance of service is used in which module.
This same campaignService without Redux could return different data in 2 modules, because it’s using new instance.
With centralized store you don’t care is it same service instance or not, cause store is always the same and service is only accessing data inside the store.
I don’t recommend intentionally creating many instances of same service (service should be singleton) but sometimes you just can’t complete the task without it.

Live Demo and Source code

I’ve been asked about code for this tutorial so I created GitHub repository with entire code and few extras.
I also created Live demo and deployed it on GitHub Pages.
Live Demo Screenshot
I didn’t remove DevTools module from the deploy, so you will be able to track store changes if you install DevTools Crhome extension.

Closing

I am pretty new to Angular 2, I guess it couldn’t be different :) and many concepts were new to me. What I liked about it is TypeScript and strong conventions introduced.
On small projects these conventions are too much, but for any larger project they are welcomed.
I would like to hear how you complete similar tasks in your own projects, do you use Redux? What about requests to backend?

No comments: