Redux and Redux-Observable in Angular applications

Andriy Kaminskyy, Software Developer at Bitcom Systems on 2017-12-25

Introduction

As modern front-end applications grow bigger and more complex, the amounts of data managed by them is growing too. Managing state from each Angular component works for small applications, but this approach tends to be tedious and burdensome for bigger ones. Redux is one way to solve this problem.

About Redux

Redux is a simple storage for your application state.

Key features of Redux are:

  • All of your application state is stored in a single place called store
  • Your state is immutable and cannot be changed directly, instead you must use a set of predefined actions, which describe what happened in the application
  • New state is created by combining your current state with actions, which is done by a special function called reducer

redux architecture

You can learn more about redux here.

Different Approaches

There are two popular packages that provide Redux functionality to Angular applications.

@angular/redux

This package is a set of bindings for Angular which makes it easier to integrate Redux into your Angular application and is fully compatible with all Redux middleware.

Middleware is another way to say plugins. Basically, it is a third party extension which operates on actions before they reach the reducer. People might use it for logging, crash reporting, talking to an asynchronous API (more on that later), routing, and more.

@angular/redux also introduces a couple of additional functions, which help bridging the gap with some of Angular’s advanced features, namely:

  • Change processing using RxJs Observables
  • Angular ngModule optimizations and AOT compilation
  • Integration with Angular’s change detection

Link to @angular/redux documentation.

@ngrx/store

This package, on the other hand, is a complete reimplementation of Redux, build from the ground up using RxJs. Unfortunately, it’s not fully compatible with existing Redux ecosystem, because its API is a bit different. The main advantage of @ngrx/store is that it is written using Angular conventions and has deeper integration with RxJs.

Link to @ngrx/store documentation.

Basic example

In this example we will make a simplistic Todo list app:

Todo list application

To get started you need to install @angular/redux and its only peer dependency - Redux.

$ npm install redux @angular-redux/store —save

Application State and Actions

Then we need to define our application state and a set of actions to interact with it. Here’s our interface for state:

state.ts:

export interface IAppState {
  todos: ITodo[];
}
export interface ITodo {
  name: string;
  description: string;
  date: Date;
}
export const INITIAL_STATE: IAppState = {
  todos: []
};

As you can see, our store contains a list of tasks, every one of which consists of name, description and a date. Now let’s define our actions:

app.actions.ts:

import { Injectable } from '@angular/core';
import { AnyAction } from 'redux';

@Injectable()
export class TodoActions {
  static ADD_TODO = 'ADD_TODO';
  static REMOVE_TODO = 'REMOVE_TODO';

  addTodo(name: string, description: string): AnyAction {
    return { type: TodoActions.ADD_TODO, name, description};
  }

  removeTodo(index: number): AnyAction {
    return { type: TodoActions.REMOVE_TODO, index };
  }
}

We created an Angular service with two functions that return actions. As you can see, actions are plain old JavaScript objects with a type member on them. Other members (name, description and index) are used for data and can be named whatever you like.

Functions that create new actions are commonly called action creators in the Redux community. They are not necessary though, you can create actions by yourself.

ADD_TODO action instructs the reducer to create a new todo task with the data provided in our action.
REMOVE_TODO action instructs the reducer to remove todo task with a specified index.

Reducer

After that, we can create a reducer that will tie our store and actions together.

reducer.ts:

import { AnyAction } from 'redux';
import { TodoActions } from './app.actions';

export interface IAppState {      // state that we
  todos: ITodo[];                 // defined earlier
}
export interface ITodo {
  name: string;
  description: string;
  date: Date;
}
export const INITIAL_STATE: IAppState = {
  todos: []
};

export function rootReducer(lastState: IAppState, action: AnyAction): IAppState {
  switch(action.type) {
    case TodoActions.ADD_TODO: 
      return { 
        todos: [ 
          ...lastState.todos,
          {
            name: action.name, 
            description: action.description, 
            date: action.date
          }
        ] 
      };
    case TodoActions.REMOVE_TODO:
      return {
        todos: [
          ...lastState.todos.slice(0, action.index), 
          ...lastState.todos.slice(action.index + 1)
        ]
      };
  }
  return lastState;
}

The rootReducer is a function that takes two arguments: lastState is old application state and action is one of actions we defined earlier. Reducer returns new application state.

You should never return modified old state in reducer, instead each reducer call must return a brand new object. Redux expects to get new objects every time when something changes in the state. You may return the same object only if you didn’t change anything in it.

Store

Finally, we can create our state and start using Redux in the app. Also don’t forget to add TodoActions in the AppModule.

app.module.ts:

import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { NgReduxModule, NgRedux } from '@angular-redux/store';
import { rootReducer, IAppState, INITIAL_STATE } from './reducer';
import { TodoActions } from './app.actions';

@NgModule({
  imports:      [ BrowserModule, NgReduxModule, FormsModule ],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ],
  providers: [ TodoActions ]
})
export class AppModule {
  constructor(ngRedux: NgRedux<IAppState>) {
    ngRedux.configureStore(
      rootReducer,
      INITIAL_STATE);
  }
}

ngRedux.configureStore is a function that creates a new store for us and takes our reducer and initial state of the app as arguments and creates a new store out of them.

Using the store

Here’s our component that will display a list of tasks and a form to add new one.

app.component.ts:

import { Component } from '@angular/core';
import { NgRedux } from '@angular-redux/store';
import { TodoActions } from './app.actions';
import { IAppState, ITodo } from "./state";
import { OnDestroy } from '@angular/core';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnDestroy {
  todos: ITodo[];
  currentName: string;
  currentDescription: string;
  subscription;

  constructor(
    private ngRedux: NgRedux<IAppState>,
    private actions: TodoActions) {
      this.subscription = ngRedux.select<ITodo[]>('todos')
        .subscribe(newTodos => this.todos = newTodos);
  }
  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
  addTodo() {
    this.ngRedux.dispatch(
      this.actions.addTodo(this.currentName, this.currentDescription)
    );
  }
  removeTodo(index: number) {
    this.ngRedux.dispatch(this.actions.removeTodo(index));
  }
}

app.component.html:

<h1>ToDo List</h1>
<div class="container">
  <div class="inputs">
    <h2>Add new task:</h2>
    <label>Task name: </label>
    <input type="text" [(ngModel)]="currentName" />
    <label>Task desctiption: </label>
    <input type="text" [(ngModel)]="currentDescription" />
    <button (click)="addTodo()">Add todo</button>
  </div>
  <div class="tasks">
    <h2>Tasks:</h2>
    <div class="task" *ngFor="let todo of todos; let i = index">
      <h3>{{todo.name}} 
        <button (click)="removeTodo(i)">Done</button>
      </h3>
      <p>{{todo.description}}</p>
      <p>Added on: {{todo.date | date: 'dd/MM/yy'}}</p>
    </div>
  </div>
</div>

CSS is omitted for brevity.
We use ngRedux service to access our store and dispatch (send) our actions. ngRedux.select is a handy method, that exposes parts of our store data via RxJs Observable.
ngRedux.dispatch function takes action object as an argument and applies it to our rootReducer in order to update our app state.

Here’s a finished application:

Asynchronous data

Redux community already provides many different ways of handling async data (mainly http requests). Some of notable examples are Redux Thunk and Redux Saga. However, if your application uses RxJs (which present in all Angular applications) you can use Redux-Observable. We will use Redux-Observable middleware to make our todo application fetch data from an API.

For demonstration purposes, I’ve made a fake JSON API, which returns this response:

[
  {"name": "Buy eggs", "description": "Buy it today", "date": "2017-12-23T18:25:43.511Z"},
  {"name": "Buy bread", "description": "Buy it tomorrow", "date": "2017-12-22T18:25:43.511Z"},
  {"name": "Buy butter", "description": "Buy it next week", "date": "2017-11-28T18:25:43.511Z"}
]

The core concept of Redux-Observable is called an epic. Epic is a function takes a stream of actions and returns a stream of actions. Here is an example of a simple epic:

const pingEpic = action$ =>
  action$.filter(action => action.type === 'PING')
    .delay(1000) // Asynchronously wait 1000ms then continue
    .mapTo({ type: 'PONG' });

This epic filters actions by type of PING and dispatches a PONG action in a second.

A variable with a dollar sign at the end (action$) is common RxJs convention to indicate that the variable is a stream.

Epics make async operations very easy.

So let’s start by installing redux-observable:

npm install --save redux-observable

Now we should write an epic that will fetch the list of todos for us and actions that would indicate the request to fetch data and fulfilment of that request:

app.actions.ts:

import { Injectable } from '@angular/core';
import { AnyAction } from 'redux';
import { ajax } from 'rxjs/observable/dom/ajax'; 

const fetchListFulfilled = payload => ({
    type: TodoActions.FETCH_LIST_FULFILLED, 
    todos: payload
  });

export const epic = (action$) =>
  action$.ofType(TodoActions.FETCH_LIST)
    .mergeMap(action => 
      ajax.getJSON('https://www.mocky.io/v2/5a421404300000fe0c709d12') // Our API
        .map(response => fetchListFulfilled(response))
    )

@Injectable()
export class TodoActions {
  static ADD_TODO = 'ADD_TODO';
  static REMOVE_TODO = 'REMOVE_TODO';
  static FETCH_LIST = 'FETCH_LIST';
  static FETCH_LIST_FULFILLED = 'FETCH_LIST_FULFILLED';

  fetchList() {
    return { type: TodoActions.FETCH_LIST };
  }
  addTodo(name: string, description: string): AnyAction {
    return { type: TodoActions.ADD_TODO, name, description, date: new Date()};
  }
  removeTodo(index: number): AnyAction {
    return { type: TodoActions.REMOVE_TODO, index };
  }
}

FETCH_LIST is an action that requests to fetch a list of todos from API. This action will be ignored by the reducer, but used by epic.
FETCH_LIST_FULFILLED is an action that is dispatched on a successful request from API and has data inside.
Our epic here catches all FETCH_LIST actions and maps them to FETCH_LIST_FULFILLED actions, that contain data from remote server. So, between FETCH_LIST and FETCH_LIST_FULFILLED actions being sent, our epic makes an ajax request. Also, I used RxJs built-in ajax object to fetch data.

Next, we need to add Redux-Observable middleware to our Redux store, so that it would dispatch our actions to epic, as well as to the reducer.

app.module.ts:

...
import { TodoActions, epic } from './app.actions';
import { createEpicMiddleware } from 'redux-observable';

@NgModule({
  imports:      [ BrowserModule, NgReduxModule, FormsModule ],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ],
  providers: [ TodoActions ]
})
export class AppModule {
  constructor(ngRedux: NgRedux<IAppState>) {
    ngRedux.configureStore(
      rootReducer,
      INITIAL_STATE,
      [createEpicMiddleware(epic)]
      );
  }
}

And, finally, add a new case to reducer for FETCH_LIST_FULLFILLED action:

reducer.ts:

...

export function rootReducer(lastState: IAppState, action: AnyAction): IAppState {
  switch(action.type) {
    case TodoActions.FETCH_LIST_FULFILLED:
      return {todos: [...action.todos, ...lastState.todos]}

...  // rest is the same

Now we can dispatch an action to load a list of todos in our component’s onInit.

app.component.ts:

...

ngOnInit() {
  this.ngRedux.dispatch(this.actions.fetchList());
}

... // rest is the same

And here is our final application with preloaded tasks:

Conclusion

After reading this article you should be able to organize your application state into a Redux store, access it in Angular components and fetch data from a remote server into state using Redux-Observables. Don’t hesitate to look for more info on the subject as we barely scratched the surface here.