AngularJS 2.0 From The Beginning - State Management - Day Twenty Three

In this Angular 2.0 article series, we have already discussed about different types of basic and advanced concepts or features of AngularJS 2.0 like data binding, directives, pipes, service, route, http modules, change detection etc. Now, in this article, we will discuss about State Management concept in AngularJS.

In case you have not had a look at the previous articles of this series, go through the links mentioned below.

For larger Angular applications with a lot of asynchronous activity and where there's a lot of states that are being shared and manipulated across multiple components and modules, managing state can be quite challenging. In a typical application, we're managing things like:

  • Data that comes from the server and whether it's pending or results in an error
  • UI state like toggles, alerts and errors messages
  • User input, such as form submissions, filters and search queries
  • Custom themes, credentials and localization
  • Many other types of state

As the application grows, how do we know that a state change in one module will consistently and accurately reflect in other modules? And what if these modifications result in even more state changes? Eventually, it becomes extremely difficult to reason about what's actually happening in your application, and be a large source of bugs.

In Angular, we can solve this problem by using Redux using @ngrx.

What is Redux?

Redux is an application state manager for JavaScript applications, and keeps with the core principles of the Flux-architecture by having a unidirectional data flow in your application. Where Flux applications traditionally have multiple stores, Redux applications have only one global, read-only application state. This state is calculated by "reducing" over a collection or stream of actions that update it in controlled ways.

What is @ngrx?

Redux state managers have been very well received and have inspired the creation of @ngrx, a set of modules that implement the same way of managing state as well as some of the middleware and tools in the Redux ecosystem. @ngrx was created to be used specifically with Angular and RxJS, as it leans heavily on the observable paradigm.

Adding @ngrx to your Project

In your console, run the following command to add @ngrx to your list of dependencies in package.json.

  1. npm install @ngrx/core @ngrx/store --save   

If you plan on using the @ngrx/effects extensions to add side-effect capabilities, then also run the following command.

  1. npm install @ngrx/effects --save   

Defining Main Application State

When building an application using Redux, the first thing to think about is, "What state do I want to store?" It is generally a good idea to capture all of the application's states so that it can be accessible from anywhere and all in one place for easy inspection.

In the application state, we store things like:

  • Data received through API calls
  • User input
  • Presentation state, such as menu and button toggles
  • Application preferences
  • Internationalization messages
  • Themes and other customizable areas of your application

To define your application state, use an interface called AppState or IAppState, depending on the naming conventions used on your project.

Here's an example.

app/models/appState.ts

  1. export interface AppState {  
  2.   readonly colors: Colors;  
  3.   readonly localization: Localization;  
  4.   readonly login: Login;  
  5.   readonly projectList: ProjectList;  
  6.   readonly registration: Registration;  
  7.   readonly showMainNavigation: boolean;  
  8. }   

Example Application

In this chapter, you'll be creating a simple counter application using @ngrx. Your app will allow users to increment and decrement a number by one, as well as reset that value back to zero. Here's the AppState that we'll be using throughout the example.

app/models/appState.ts

  1. import {Counter} from './counter';  
  2.   
  3. export interface AppState {  
  4.   readonly counter: Counter;  
  5. }  

app/models/counter.ts

  1. export interface Counter {  
  2.   readonly currentValue: number;  
  3. }  

Reading your Application State using Selectors

To read your application state in Redux, we need to use the select() method on @ngrx's Store class. This method creates and returns an Observable that is bound to a specific property in your application state. For example, here's how you would select the counter object:

         store.select('counter'); // Returns Observable<Counter>

And to fetch the counter's currentValue, we can pass in a string array, where each string plucks a single property from the application state one at a time in the order specified:

         store.select(['counter', 'currentValue']); // Returns Observable<number>

While select() allows for several variations of strings to be passed in, it has it' shortcomings - namely you won't actually know if the plucking is working properly until you execute your code. Because of that, select() allows you to select values using functions too, which makes things more type-safe and your selectors will be more refactorable by your IDE.

         store.select(appState => appState.counter.currentValue);

Creating a Counter Service

While you could inject Store and select values directly in your Angular components, it's considered to be a best practice to wrap this functionality into separate services. This approach encapsulates all of the selection logic and eliminates any duplication where the selection path is repeated throughout your application.

Let's tie everything together by building out a CounterService example:

app/services/counter.service.ts

  1. import {Injectable} from '@angular/core';  
  2. import {Store} from '@ngrx/store';  
  3. import {Observable} from 'rxjs/Observable';  
  4. import {AppState} from '../models';  
  5.   
  6. @Injectable()  
  7. export class CounterService {  
  8.   constructor(private store: Store<AppState>) {}  
  9.   getCurrentValue(): Observable<number> {  
  10.     return this.store.select(appState => appState.counter.currentValue)  
  11.       .filter(Boolean);  
  12.   }  
  13. }  

Because select() returns an Observable, the getCurrentValue() method also applies a filter() to ensure that subscribers do not receive any falsy values. This greatly simplifies the code and templates in your components, since they don't have to repeatedly consider the false case everywhere the value is used.

Actions

Redux uses a concept called Actions, which describe state changes to your application. Redux actions are simple JSON objects that implement the Action interface provided by @ngrx,

  1. export interface Action {  
  2.   type: string;  
  3.   payload?: any;  
  4. }  

The type property is a string used to uniquely identify your action to your application. It's a common convention to use lisp-case (such as MY_ACTION), however you are free to use whatever casing style that makes to your team, as long as it's consistent across the project. The payload property provides a way to pass additional data to other parts of Redux, and it's entirely optional. Here is an example,

  1. const loginSendAction: Action = {  
  2.   type: 'LOGIN_SEND',  
  3.   payload: {  
  4.     username: 'katie',  
  5.     password: '35c0cd1ecbbb68c75498b83c4e79fe2b'  
  6.   }  
  7. };  

Plain objects are used so that the actions are serializable and can be replayable into the application state. Even if your actions involve asynchronous logic, the final dispatched action will remain a plain JSON object.To simplify action creation, you can create a factory function to take care of the repeating parts within your application.

app/store/createAction.ts

  1. import {Action} from '@ngrx/store';  
  2.   
  3. export function createAction(type, payload?): Action {  
  4.   return { type, payload };  
  5. }  
The resulting creation of the LOGIN_SEND action becomes much more succinct and cleaner,
  1. const loginSendAction: Action = createAction('LOGIN_SEND', {  
  2.   username: 'katie',  
  3.   password: '35c0cd1ecbbb68c75498b83c4e79fe2b'  
  4. });  

Modifying Application State by Dispatching Actions

Most Redux apps have a set of functions, called "action creators", that are used to set up and dispatch actions. In Angular, it's convenient to define your action creators as @Injectable() services, decoupling the dispatch, creation and side-effect logic from the @Component classes in your application.

Synchronous Actions - Here is a simple example.

app/store/counter/counter.actions.ts

  1. import {Injectable} from '@angular/core';  
  2. import {Store} from '@ngrx/store';  
  3.   
  4. import {createAction} from '../createAction';  
  5. import {AppState} from '../../models/appState';  
  6.   
  7. @Injectable()  
  8. export class CounterActions {  
  9.   static INCREMENT = 'INCREMENT';  
  10.   static DECREMENT = 'DECREMENT';  
  11.   static RESET = 'RESET';  
  12.   constructor(private store: Store<AppState>) {  
  13.   }  
  14.   
  15.   increment() {  
  16.     this.store.dispatch(createAction(CounterActions.INCREMENT));  
  17.   }  
  18.   
  19.   decrement() {  
  20.     this.store.dispatch(createAction(CounterActions.DECREMENT));  
  21.   }  
  22.   
  23.   reset() {  
  24.     this.store.dispatch(createAction(CounterActions.RESET));  
  25.   }  
  26. }  

As you can see, the action creators are simple functions that dispatch Action objects containing more information that describes the state modification.

Asynchronous Actions

This "ActionCreatorService" pattern comes in handy if you must handle asynchronous or conditional actions (users of react-redux may recognize this pattern as analogous to redux-thunk in a dependency-injected world).

app/store/counter/counter.actions.ts

  1. import {Injectable} from '@angular/core';  
  2. import {Store} from '@ngrx/store';  
  3. import {createAction} from '../createAction';  
  4. import {AppState} from '../../models/appState';  
  5.   
  6. @Injectable()  
  7. export class CounterActions {  
  8.   
  9.   constructor(private store: Store<AppState>) {  
  10.   
  11.   }  
  12.   
  13.   incrementIfOdd() {  
  14.     this.store.select(appState => appState.counter.currentValue)  
  15.       .take(1)  
  16.       .subscribe(currentValue => {  
  17.         if (currentValue % 2 !== 0) {  
  18.           this.store.dispatch(createAction(CounterActions.INCREMENT);  
  19.         }  
  20.       });  
  21.   }  
  22.   
  23.   incrementAsync(timeInMs: number = 1000) {  
  24.     this.delay(timeInMs).then(() => this.store.dispatch(createAction(CounterActions.INCREMENT)));  
  25.   }  
  26.   
  27.   private delay(timeInMs: number) {  
  28.     return new Promise((resolve) => {  
  29.       setTimeout(() => resolve() , timeInMs);  
  30.     });  
  31.   }  
  32. }  

In the incrementIfOdd() action creator, we create one-time subscription to the counter's currentValue in the application state. From there, we check to see if it's odd before dispatching an action. In the incrementAsync() action creator, we are delaying the actual call to dispatch(). We created a Promise that will resolve after the delay. Once the Promise resolves, we can then dispatch an action to increment the counter. 

Actions that Depend on Other Services

The ActionCreatorService pattern becomes necessary in cases where your action creators must use other Angular services. Consider the following SessionActions service that handles a remote API call.

  1. import {Injectable} from '@angular/core';  
  2. import {Store} from '@ngrx/store';  
  3.   
  4. import {createAction} from '../createAction';  
  5. import {AppState} from '../../models/appState';  
  6.   
  7. @Injectable()  
  8. export class SessionActions {  
  9.   static LOGIN_USER_PENDING = 'LOGIN_USER_PENDING';  
  10.   static LOGIN_USER_SUCCESS = 'LOGIN_USER_SUCCESS';  
  11.   static LOGIN_USER_ERROR = 'LOGIN_USER_ERROR';  
  12.   static LOGOUT_USER = 'LOGOUT_USER';  
  13.   
  14.   constructor(  
  15.     private store: Store<AppState>,  
  16.     private authService: AuthService  
  17.   ) {  
  18.   }  
  19.   
  20.   loginUser(credentials: any) {  
  21.     this.store.dispatch(createAction(SessionActions.LOGIN_USER_PENDING));  
  22.     this.authService.login(credentials.username, credentials.password)  
  23.       .then(result => this.store.dispatch(createAction(SessionActions.LOGIN_USER_SUCCESS, result)))  
  24.       .catch(() => this.store.dispatch(createAction(SessionActions.LOGIN_USER_ERROR)));  
  25.   };  
  26.   
  27.   logoutUser() {  
  28.     this.store.dispatch(createAction(SessionActions.LOGOUT_USER));  
  29.   };  
  30. }  

Review of Reducers and Pure Functions

One of the core concepts of Redux is the reducer. A reducer is a function with the signature (accumulator: T, item: U) => T. Reducers are often used in JavaScript through the Array.reducemethod, which iterates over each of the array's items and accumulates a single value as a result. Reducers should be pure functions, meaning they don't generate any side-effects. A simple example of a reducer is the sum function,

let x = [1, 2, 3].reduce((sum, number) => sum + number, 0);

Reducers as State Management

Reducers are simple ideas that turn out to be very powerful. With Redux, you replay a series of actions into the reducer and get your new application state as a result. Reducers in a Redux application should not mutate the state, but return a copy of it, and be side-effect free. This encourages you to think of your application as UI that gets "computed" from a series of actions in time.

Simple Reducer - Let's take a look at a simple counter reducer.

app/store/counter/counter.reducer.ts

  1. import {Action} from '@ngrx/store';  
  2. import {CounterActions} from './counter.actions';  
  3.   
  4. export default function counterReducer(state: number = 0, action: Action): number {  
  5.   switch (action.type) {  
  6.     case CounterActions.INCREMENT:  
  7.       return state + 1;  
  8.     case CounterActions.DECREMENT:  
  9.       return state - 1;  
  10.     case CounterActions.RESET:  
  11.       return 0;  
  12.     default:  
  13.       return state;  
  14.   }  
  15. }  

We can see here that we are passing in an initial state (the current number) and an Action. To handle each action, a common approach is to use a switch statement. Instead of each reducer needing to explicitly subscribe to the dispatcher, every action gets passed into each reducer, which handles the actions it's interested in and then returns the new state along to the next reducer. Reducers should be side-effect free. This means that they should not modify things outside of their own scope. They should simply compute the next application state as a pure function of the reducer's arguments. For this reason, side-effect causing operations, such as updating a record in a database, generating an id, etc. should be handled elsewhere in the application, like in your action creators or using @ngrx/effects

Complex Reducer

Another consideration when creating your reducers is to ensure that they are immutable and not modifying the state of your application. If you mutate your application state, it can cause unexpected behavior. There are a few ways to help maintain immutability in your reducers. One way is by using new ES6 features such as Object.assign() or the spread operator for arrays.

app/models/counter.ts
  1. export function setCounterCurrentValue(counter: Counter, currentValue: number): Counter {  
  2.   return Object.assign({}, counter, { currentValue });  
  3. }  

Here, the setCounterCurrentValue() function creates a new Counter object that overwrites the counter.currentValue property with a new value while maintaining the references and values of all of the other properties from counter. Let's update our reducer to utilize this concept,

  1. import {Action} from '@ngrx/store';  
  2. import {Counter, createDefaultCounter, setCounterCurrentValue} from '../../models/counter';  
  3. import {CounterActions} from './counter.actions';  
  4.   
  5. export function counterReducer(  
  6.   counter: Counter = { currentValue: 0 },   
  7.   action: Action  
  8. ): Counter {  
  9.   switch (action.type) {  
  10.     case CounterActions.INCREMENT:  
  11.       return setCounterCurrentValue(counter, counter.currentValue + 1);  
  12.   
  13.     case CounterActions.DECREMENT:  
  14.       return setCounterCurrentValue(counter, counter.currentValue - 1);  
  15.   
  16.     case CounterActions.RESET:  
  17.       return setCounterCurrentValue(counter, 0);  
  18.   
  19.     default:  
  20.       return counter;  
  21.   }  
  22. }  

With each action, we take the existing counter state and create a new state with the updated value (such as counter.currentValue + 1). When dealing with complex or deeply nested objects, it can be difficult to maintain immutability in your application using this syntax. This is where a library like Ramda can help.

Creating your Application's Root Reducer

@ngrx allows us to break our application into smaller reducers with a single area of concern. We can combine these reducers by creating an object that mirrors the application's AppState, where each property will point to one of those smaller reducers.

app/store/rootReducer.ts

  1. import {counterReducer} from './counter/counter.reducer';  
  2.   
  3. export const rootReducer = {  
  4.   counter: counterReducer  
  5. };  

Configuring your application

Once you have your reducers created, it’s time to configure your Angular application. In your main application module, simple add the StoreModule.provideStore() call to your @NgModule's imports:

app/app.module.ts

  1. import {BrowserModule} from '@angular/platform-browser';  
  2. import {NgModule} from '@angular/core';  
  3. import {FormsModule} from '@angular/forms';  
  4. import {HttpModule} from '@angular/http';  
  5. import {StoreModule} from '@ngrx/store';  
  6. import {EffectsModule} from '@ngrx/effects';  
  7. import 'rxjs/Rx';  
  8. import {rootReducer} from './store/rootReducer';import {CounterActions} from './store/actions';  
  9. import {CounterEffects} from './store/effects';  
  10. import {AppComponent, CounterComponent} from './components';  
  11. import {CounterService} from './services';  
  12.   
  13. @NgModule({  
  14.   imports: [  
  15.     BrowserModule,  
  16.     FormsModule,  
  17.     HttpModule,  
  18.     StoreModule.provideStore(rootReducer)  
  19.   ],  
  20.   declarations: [  
  21.     AppComponent,  
  22.     CounterComponent  
  23.   ],  
  24.   providers: [  
  25.     CounterActions,  
  26.     CounterService  
  27.   ],  
  28.   bootstrap: [AppComponent]  
  29. })  
  30. export class AppModule {  
  31.   
  32. }  

Implementing Components

To demonstrate how to use the CounterService in your components, let's start by building out a small CounterComponent. The component will be responsible for incrementing and decrementing the counter by one, as well as allowing the user to reset the counter to zero.

app/components/counter.component.ts

  1. import {Component, Input} from '@angular/core';  
  2. import {Observable} from 'rxjs/Observable';  
  3. import {CounterService} from '../services';  
  4. import {CounterActions} from '../store/counter/counter.actions';  
  5.   
  6. @Component({  
  7.   selector: 'counter',  
  8.   templateUrl: './counter.component.html'  
  9. })  
  10. export class CounterComponent {  
  11.   private currentValue$: Observable<number>;  
  12.   constructor(  
  13.     counterService: CounterService,  
  14.     public actions: CounterActions  
  15.   ) {  
  16.     this.currentValue$ = counterService.getCurrentValue();  
  17.   }  
  18. }  

app/components/counter.component.html

  1. <p>  
  2.   Clicked: {{currentValue$ | async}} times  
  3.   <button (click)="actions.increment()">+</button>  
  4.   <button (click)="actions.decrement()">-</button>  
  5.   <button (click)="actions.reset()">Reset</button>  
  6. </p>  

The template syntax should be familiar by now, displaying an Observable counter value with the asyncpipe. Any time appState.counter.currentValue is updated by a reducer, currentValue$ will receive the new value and | async will update it in the template. The component also handles some click events. Each click event is bound to expressions that call our action creators from the CounterActions ActionCreatorService.

Modifying AppComponent to become a smart component

First, let's modify our top-level application component to use the CounterService and CounterActions, just as CounterComponent did,

app/app.component.ts

  1. import {Component} from '@angular/core';  
  2. import {Observable} from 'rxjs';  
  3.   
  4. import {Counter} from '../../models/counter';  
  5. import {CounterService} from '../../services/counter.service';  
  6. import {CounterActions} from '../../store/counter/counter.actions';  
  7.   
  8. @Component({  
  9.   selector: 'app-root',  
  10.   templateUrl: './app.component.html',  
  11.   styleUrls: ['./app.component.css']  
  12. })  
  13. export class AppComponent {  
  14.   
  15.   counter$: Observable<Counter>;  
  16.   
  17.   constructor(  
  18.     counterService: CounterService,  
  19.     public actions: CounterActions  
  20.   ) {  
  21.     this.counter$ = counterService.getCounter();  
  22.   }  
  23.   
  24. }  

Now, our AppComponent is a smart-component, because it's aware of Redux, it's presence in the application state and the underlying services. As with previous examples, we can use the async pipe to obtain the most recent counter value and pass it along to other components within the template. And while we haven't looked at the @Output()'s on CounterComponent just yet, we'll want to delegate those events to our action creators in CounterActions.

app/app.component.html

  1. <counter [counter]="counter$ | async"  
  2.          (onIncrement)="actions.increment()"  
  3.          (onDecrement)="actions.decrement()"  
  4.          (onReset)="actions.reset()">  
  5. </counter>  

Modifying CounterComponent to become a presentation component

In turn, we need to make the CounterComponent from a smart component into a dumb component. For this, we will pass the data into the component using @Input properties and click events using @Output() properties, removing the use of CounterService and CounterActions entirely.

app/counter/counter.component.ts

  1. import {Component, Input, EventEmitter, Output} from '@angular/core';  
  2.   
  3. import {Counter} from '../../models/counter';  
  4.   
  5. @Component({  
  6.   selector: 'counter',  
  7.   templateUrl: './counter.component.html'  
  8. })  
  9. export class CounterComponent {  
  10.   
  11.   @Input()  
  12.   counter: Counter;  
  13.   
  14.   @Output()  
  15.   onIncrement: EventEmitter<void> = new EventEmitter<void>();  
  16.   
  17.   @Output()  
  18.   onDecrement: EventEmitter<void> = new EventEmitter<void>();  
  19.   
  20.   @Output()  
  21.   onReset: EventEmitter<void> = new EventEmitter<void>();  
  22.   
  23. }  

Our child components become much simpler and testable, because we don't have to use the async pipe to work with our state, which removes a lot of pain when dealing with lots of @Input's or the need to use complex expressions with Observables. We can also now simply use core Angular features to emit values whenever a click event happens.

app/counter/counter.component.html

  1. <p>  
  2.   Clicked: {{counter.currentValue}} times  
  3.   <button (click)="onIncrement.emit()">+</button>  
  4.   <button (click)="onDecrement.emit()">-</button>  
  5.   <button (click)="onReset.emit()">Reset</button>  
  6. </p>  

We now have a nicely-reusable presentational component with no knowledge of Redux or our application state.

X

Build smarter apps with Machine Learning, Bots, Cognitive Services - Start free.

Start Learning Now