+2

Connecting Redux with Angular 4+

Introduction

When you first think of Redux you also probably think of using it along with React, because that is what most people usually do and it also being use as an example on official redux site too . But the thing is it can be used with any javascript libraries as we wish and not just react because really it is just one library that help us to easily manage our application's state and that is all there is to it.

Getting Start

Lets see how to go about and integrate Redux with Angular app. What we need to do is to some how get redux's Store to be accessible to every part of our application and we do that by using Dependency Injection. Remember that in angular we can define a service and inject that service into any components or other services when we need and that service only created once so everytime we'll get the exact same service and that is perfect for our case because we only need one store per application. You can find the source code for this article in my github repository.

Mini Redux Version

In this article we will implement our own redux with typescript and hook it up with our soon to be todo app.

Action

First of lets define an interface that represent Action. Nothing fancy here we just say that an action should contain two fields with type to store what type of action and payload as an optional field which can be any type of data. Any object that conform to this interface can be passed down to reducer function.

export interface Action {
  type: string;
  payload?: any;
}

Reducer

Next on the list is Reducer. Reducer should be a function that take any application's state, hence the use of generics template, along with action and produce an new state.

export interface Reducer<T> {
  (state: T, action: Action): T;
}

Store

Ok there are alot to talk about here. Store is responsible for dispatching any user action into reducer to produce new application state and notify any clients(subscribers) that are interested in thoses changes. And we provide dispatch method for that purpose. To do that we need two fields, one to store the state and the other to store the list of callbacks. Callback should be a function that take no argument and return nothing so we also defined an interface for it too. When creating a store we can also pass in an initial state that is what the constructor arguments for. To be able to provide access to the state object we've also defined getState method which return a copy of state. Notice that we return a copy not the original and that is important because state should be read only any operations that needed to alert the app's state must be done through action. For the clients to be able to get notified we need a way for them to register themselves and that's where subscribe method come into play. What we do is add a client registered callback into a callbacks list and return an unsubscribe function back to the clients so that they can unsubscribe later on.

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

export interface StoreCallback {
  (): void;
}

@Injectable()
export class Store {
  private _state: T;
  private _callbacks: StoreCallback[] = [];

  constructor(private _reducer: Reducer<T>, initialState: T) {
    this._state = initialState;
  }

  getState(): T {
    return Object.assign({}, this._state) as T;
  }

  dispatch(action: Action): void {
    this._state = this._reducer(this._state, action);
    this._callbacks.forEach((c: StoreCallback) => c());
  }

  subscribe(callback: StoreCallback): StoreCallback {
    this._callbacks.push(callback);
    return () => {
      this._callbacks = this._callbacks.filter(c => c !== callback);
    }
  }
}

Todo App

Now that the implementation of redux is out of the way we can focus on our real goal. First hop on to command line and generate an angular project.

$ ng new todo
$ cd todo
$ npm install bootstrap@next jquery popper.js --save

App State

Todo app will provide two basic functionalities create and delete todo list and in order to do that we need a way to hold thoses todos in other word we need to define how an application state looks like. To hold many todos message we need a list of string so the app's state look like below.

export interface AppState {
  todos: string[];
}

App Reducer

There's not much to say here since this is just a typical redux's reducer. The thing to note here is that this reducer will responds to ADD_TODO and DELETE_TODO actions.

export const AppReducer: Reducer<AppState> = (state: AppState, action: Action): AppState => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        todos: state.todos.concat(action.payload)
      };

    case 'DELETE_TODO':
      let newState = [...state.todos];
      newState.splice(action.payload, 1);

      return {
        todos: newState
      };

    default:
      return state;
  }
}

Action Creator

To help making thing easier to read and eliminate some errors we also created an app specific action creator class with two class methods that will return a correct type of action. addTodo take one string argument and return ADD_TODO action with todo as payload, while deleteTodo also take one argument but this time is an index of todo in the list and return DELETE__TODO action.

export default class TodoAction {
  static addTodo(todo: string): Action {
    return {
      type: 'ADD_TODO',
      payload: todo
    } as Action;
  }

  static deleteTodo(index: number): Action {
    return {
      type: 'DELETE_TODO',
      payload: index,
    } as Action;
  }
}

Prepare for Injection

For the store to be useful we need a way to inject it into our component. And in order for angular to be able to inject a particular service it need to know how to construct that service. The mechanism behind angular's DI is to use what is called provider that and factory function. Provider is usually a class name or a token and a factory is usually a function and will construct and return an instance of that service. By default when we just pass a class into providers list when bootstraping an angular module the default factory function will be used. What that function does is it try to instantiate any arguments(if any) that needed to construct the service first and then instantiate the actual service later; and it does so recursively. So for our store object we need two arguments first is the reducer and second is initial state. We can make boths of these as injectable services so we can just pass in Store class in the provider list and let angular figure out the rest, but I find it's overkill to do that, so I take another approach.

function createAppStore(): Store<AppState> {
  return new Store<AppState>(AppReducer, { todos: [] } as AppState);
}

const appStoreProviders = [
  { provide: Store, useFactory: createAppStore }
]

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: appStoreProviders,
  bootstrap: [AppComponent]
})
export class AppModule { }

Injecting Store

Now we can use store in our component like this.

import { Store, AppStore } from '../store';

@Component({
...
})
export class AppComponent {
  todos: string[] = [];

  constructor(private _store: Store<AppState>) {
    ...
  }
}

Subscribe for Changes

To subscribe to a store just put these code into AppComponent constructor

_store.subscribe(() => {
  const state: AppState = this._store.getState() as AppState;
  this.todos = state.todos;
});

Adding / Deleting Todo

Just dispatch an appropriate action to a store and the reducer will take care of the rest for us. To get a correct action we make use of our action creator class.

import { TodoAction } from '../actions';

export class AppComponent {
  addTodo(todo: string): void {
    this._store.dispatch(TodoAction.addTodo(todo));
  }

  deleteTodo(index: number): void {
    this._store.dispatch(TodoAction.deleteTodo(index));
  }
}

Final Touch a.k.a Template

Finally time a make our app look cool by adding view to it

<div class="container">
  <div class="row">
    <div class="col-6 offset-3">
      <div class="card mt-2">
        <div class="card-body">
          <h5 class="card-title text-center">Todo List</h5>
          <div class="form-inline">
            <input class="form-control form-control-sm mr-sm-2" type="text" placeholder="Todo" #txtTodo>
            <button class="btn btn-sm btn-primary" (click)="addTodo(txtTodo.value)">Add</button>
          </div>
        </div>
        <ul class="list-group ist-group-flush">
          <li class="list-group-item d-flex justify-content-between align-items-center" *ngFor="let todo of todos; let i = index">
            <div class="pull-left">{{todo}}</div>
            <button class="btn btn-sm btn-danger pull-right" (click)="deleteTodo(i)">Delete</button>
          </li>
        </ul>
      </div>
    </div>
  </div>
</div>

Connect with Real Redux

So how do we use real redux in our app? It's simple really! just change our the way to register and construct our store service provider. First off stop using our implementation of Store instead import it from Redux library.

import { Store } from 'redux';

then modify createAppStore factory function and because Store now is an interface and we can't use interface as provider we also need to create a token provider.

export const AppStore = new InjectionToken('App.Store');

function createAppStore(): Store<AppState> {
  return createStore<AppState>(
    AppReducer,
    { todos: [] } as AppState
  );
}

const appStoreProviders = [
  { provide: AppStore, useFactory: createAppStore }
]

then everytime we inject we need to switch to use an AppStore injection token instead. And that's all there is to it!

constructor(@Inject(AppStore) private _store: Store<AppState>) {
  ...
}

Further Reading

In the next article I'll show you how to use another library for state managment that use the same principle as redux, but build specifically for Angular and leverage the power of Reactive programming library called RxJS.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí