Angular4 Redux

2017-12-17

Why Redux?

There are many advantages to using a redux based architecture, here is just a few

  • Unit testing becomes much easier and saner when using redux. Q: Why? A: because for the most part we are testing pure functions and actions
  • Single state store. Every aspect of the app is stored in a single point in memory, for example if we have a list of n objects and 2 components want to read from the list they can easily read from the same store with Redux, In the past both components would have to read from the server through a http request.
  • Dumb components. With Redux we can truly have dumb components. For example if a component requires a list of n objects from the server it will dispatch an event to Redux, it will then continue working as normal, it does not care where the list comes from or what the method of retrieving the list is, i.e. it could be a websocket or an ajax request. This is very important as it means we can focus on what our component will look like rather than focusing on the means of retrieving data.
  • Separation of concerns Taking from the point above we now have an architecture where the component is 100% decoupled from any service.
  • Middleware Redux comes with middleware support and there are already great tools available to help in development. One of the most notable tools is here - https://github.com/gaearon/redux-devtools
  • Single source of truth Most logic is located in a reducer, this is a well defined location in a redux architecture, if something goes wrong most likely this will be the place to look for the issue. Why is this important? Well rather than having to look through many components or services to find the issue we now have a single source of truth.

How do I use Redux in my project?

The library I have chosen for Redux is - https://github.com/angular-redux/store, You will need to add a few libraries to your project.

yarn add redux @angular-redux/store

Setup

src/app/app.module.ts

import { NgModule } from '@angular/core';
import { NgReduxModule, NgRedux } from '@angular-redux/store'; // <- New

@NgModule({
  imports: [
    NgReduxModule // <- New
  ],
  providers: [],
})
export class AppModule { }

Action Types

For the next part imagine we are creating a feature in our project that retrieves a list of release toggles from the server. usually we create a new folder like src/app/release-toggles. Now we need to imagine what the actions for this feature might be. For example we need an action to start requesting data from the server then we need an action to show when we have got a response and an action for a failed request. create a new file to model this src/app/release-toogles/toggles.action-types.ts

export const FETCH_RELEASE_TOGGLES = 'FETCH_RELEASE_TOGGLES';
export const FETCH_RELEASE_TOGGLES_SUCCESS = 'FETCH_RELEASE_TOGGLES_SUCCESS';
export const FETCH_RELEASE_TOGGLES_FAIL = 'FETCH_RELEASE_TOGGLES_FAIL';

State structure

next we need a state file to describe what this slice of store state will look like. We can create a new file src/app/release-toggles/toggles.state.ts

export const togglesState: TogglesState = {
    isFetchingToggles: false,
    fetchFailed: false;
    toggleList: []
}

export interface ToggleState {
    isFetchingToggles: boolean;
    fetchFailed: boolean;
    toggleList: Toggle[];
}

export interface Toggle {
    category: string;
    name: string;
    description: string;
    active: boolean;
    created: string;
}

Notice the 2 interfaces here, these are an important feature of typescript and help describe the object so the typescript compiler can report any violations that it sees, we also get a lot of intellisense in our IDE by making the compiler aware of the structure and types of our objects. More info here - https://www.typescriptlang.org/docs/handbook/interfaces.html

Reducer

This is the "single source of truth" file that was mentioned above. The important thing about this file is that it always returns a new state.

State is immutable, it should not be updated directly, this would be a violation of redux. The reducer returns a new state, but does not directly update the existing state. It is very important to play by this rule. If you try to manipulate state in any way directly you will introduce hard to find bugs

Create a new file src/app/release-toggles/toggles.reducer.ts

import {FETCH_RELEASE_TOGGLES, FETCH_RELEASE_TOGGLES_SUCCESS, FETCH_RELEASE_TOGGLES_FAIL} from './toggles.action-types';
import {TogglesState, togglesState} from './toggles.state';

export const togglesReducer = (state: TogglesState = togglesState, action) => {
    switch(action.type) {
        case FETCH_RELEASE_TOGGLES:
            return {...state, isFetchingToggles: true};
        case FETCH_RELEASE_TOGGLES_SUCCESS:
            return {...state, isFetchingToggles: false, toggleList: action.toggleList};
        case FETCH_RELEASE_TOGGLES_FAIL:
            return {...state, isFetchingToggles: false, fetchFailed: true};
        default:
            return state;
    }
}

Actions

This is a wrapper for the action-types above, Here is where you can also attach data to your dispatched events, for example I want to dispatch the action - FETCH_RELEASE_TOGGLES_SUCCESS but I also want to attach the list of toogles to the event. We would do this here. We can also make api requests here an dispatch other actions. Create a new file src/app/release-toggles/toggles.actions.ts

import {FETCH_RELEASE_TOGGLES, FETCH_RELEASE_TOGGLES_SUCCESS, FETCH_RELEASE_TOGGLES_FAIL} from './toggles.action-types';
import {NgRedux} from '@angular-redux/store';
import {ToggleService} from './toggles.service';

@Injectable()
export class TogglesActions {

    constructor(private ngRedux: NgRedux<IAppState>, private togglesService: TogglesService) {
    }

    fetchToggles() {
        this.ngRedux.dispatch({type: FETCH_RELEASE_TOGGLES});
        this.tasksService.getAll()
            .then(tasks => this.ngRedux.dispatch({type: FETCH_RELEASE_TOGGLES_SUCCESS,tasks}));
            .catch(() => this.ngRedux.dispatch({type: FETCH_RELEASE_TOGGLES_FAIL}));
    }
}

Components

The next question is how do we retrieve a slice of state in one of our components, this is very simple, We just need to use an @select annotation like below. Create a new file src/app/release-toggles/toggles.component.ts

import {Component, OnInit} from '@angular/core';
import {select} from '@angular-redux/store';
import {Observable} from 'rxjs/Observable';
import {TogglesState} from './toggles.state.ts';
import {TogglesActions} from './toggle.actions';

@Component({
  selector: 'app-toggle-list',
  templateUrl: './toggle-list.component.html',
  styleUrls: ['./toggle-list.component.css']
})
export class TogglesComponent implements OnInit {

  @select('toggles') toggles$: Observable<Toggle>;
  constructor(private togglesActions: TogglesActions) {
  }

  ngOnInit() {
    this.tasksListActions.getTasks();
  }
}

Template

You may have noticed above that there is a "templateUrl" in our component, this is a link to the view of our component. This is basically the html markup for that component. For example suppose we want to display a list of toggles in our template we would create a new file src/app/release-toggles/toggles.component.html

<table>
    <thead>
        <th>Name</th>
        <th>Description</th>
        <th>Active</th>
        <th>Created</th>
    </thead>
    <tbody>
        <tr *ngFor="let toggle of (toggles| async)?.toggleList">
            <td>{{toggle.name}}</td>
            <td>{{toggle.description}}</td>
            <td>{{toggle.active}}</td>
            <td>{{toggle.created}}</td>
        </tr>
    </tbody>
</table>

So the main thing here is the *ngFor loop, this is similar to ng-repeat in angular 1.x. Another important aspect is the keyword "async". This is a new powerful feature of angular4. Looking at the code above for the component we see we have toggles$. This is of type Observable, so there is a point in time where that Observable has no data, the async keyword handles this scenario gracefully and waits for a point in time when the Observable has data. In angular1.x this scenario would have thrown an error. You may also notice the question mark ?. This is called the elvis operator. It basically means that when the observable has data it may contain this toggleList or it may not, but if it does then proceed with the loop, if it doesn't then no big deal. Please see more information and discussion on the links below

https://angular.io/api/common/AsyncPipe

https://github.com/angular/angular/issues/791

Root Reducer

As you project grows you will need more and more actions to be dispatched, you may aleady have guessed that his would lead to a rather huge reducer file, to get around this there is a nice feature of redux which allows us to split reducers into smaller reducers. So there is a concept of a root reducer which combines all the little reducers spread throughout your project. To do this create a new file src/app/root-reducer.ts

import {togglesReducer as toggles} from './release-toggles/toggles.reducer';
import {combineReducers} from 'redux';

export const rootReducer = combineReducers({
    toggles
    // Add as many reducer as you like here
});

The important thing here is that you name you branch of state correctly, in this case we give it the name "toggles". Then we we select the branch in our component with the @select annotation it should match the slice of state that you want.

Finally in your app.module.ts file you can add the following to make the reducers app aware

import {NgModule} from '@angular/core';
import {NgReduxModule, NgRedux} from '@angular-redux/store';
import {rootReducer} from './root.reducer'; // <- New

@NgModule({
  imports: [
    NgReduxModule
  ],
  providers: [],
})
export class AppModule {
    constructor(private ngRedux: NgRedux<any>) {
        ngRedux.configureStore(rootReducer, {}, []);
    }
}

Now you should be up and running with Redux.

Middleware

As mentioned above redux provides lots of middleware to help in development. Here is just a few that I have picked.

redux-logger

https://github.com/evgenyrodionov/redux-logger

This is good for seeing your actions in the console as your developing. For each action dispatched it will display the following

  • Before action state of store
  • Action dispatched
  • After action state of store
yarn add -D redux-logger

Then to configure go to app.module.ts and update like below

import {NgModule} from '@angular/core';
import {NgReduxModule, NgRedux} from '@angular-redux/store';
import {createLogger} from 'redux-logger'; // <- Add this
import {rootReducer} from './root.reducer';

@NgModule({
  imports: [
    NgReduxModule
  ],
  providers: [],
})
export class AppModule {
    constructor(private ngRedux: NgRedux<any>) {
        const middleware = [
            createLogger()  // <- Add this
        ];

        ngRedux.configureStore(rootReducer, {}, middleware);
    }
}

redux-immutable-state-invariant

As mentioned above redux is great but you need to follow the rules or you will introduce bugs. You need to buy into the fact that state is immutable. One way to do this is to add this middleware that will detect any illegal state changes during development, it will then inform the user with a nice big red console message that they are violating Redux state rules. This plugin is an absolute must for building big apps.

yarn add -D redux-immutable-state-invariant

Then to configure go to app.module.ts and update like below

import {NgModule} from '@angular/core';
import {NgReduxModule, NgRedux} from '@angular-redux/store';
import * as reduxImmutableStateInvariant from 'redux-immutable-state-invariant'; // <- Add this
import {rootReducer} from './root.reducer';

@NgModule({
  imports: [
    NgReduxModule
  ],
  providers: [],
})
export class AppModule {
    constructor(private ngRedux: NgRedux<any>) {
        const middleware = [
            reduxImmutableStateInvariant.default() // <- Add this
        ];

        ngRedux.configureStore(rootReducer, {}, middleware);
    }
}