Log In

Angular Redux @ngrx/store


by Yariv Katz

A lot of complexity is added to our frontend web projects. Apps running on the browser are bigger more complex and harder to maintain. MVC is a great patterns for small and medium projects, and it is perfect for backend projects that consist mostly of a REST server, but on the frontend side organizing a big application in MVC will be really hard to maintain and scale. MVC creates a lot of side effects on many view when making changes to the model part. Our frontend projects needs a different architecture so we started creating the projects in a component based architecture. When defining a component it is easier to look at the component as a state machine. Our component has a state when state change the component will look different. This kind of thinking make our component easy to program and easy to test. Sometime our state is not internal and private in the component, sometimes the state needs to be shared between multiple components. We can also use the global state to communicate between components. We need our state to be predicatable which means every time the state is equal to something the screen will render the same thing. Also the change in the state has to be easy to control.

What is Redux

Redux is a predictable state container. Based on the Redux state our angular components will be rendered a certain way. When our Redux state will change our components will rendered accordingly. We can connect our component to certain part of the state and set the component change detection strategy to OnPush, in that case our component will re render only when the part of the state the he is connected to will change. So working with Redux and angular properly means better performance. Our state are not like global variables since our state can be splitted to sections based on our apps features, so if for example we have a Settings module holding a bunch of settings pages components and services related to the setting section in our app, we can split our state to have a settings section holding the state related stuff that the settings components need. Redux helps us turn our app to a state machine which makes stuff like testing much easier, getting the user back to where he left off means loading the state with the same state the user was when he left off, Making stuff like undo means to simply save the state changes and load the previous one. With Redux we can achieve better app structure, and better performance for our angular apps and it is a most for every complex angular application you will create.

Redux basic concepts

state - is saved in the Redux store, it is immutable and can only change by replacing the entire state.
actions - actions describe what happened in our app, might be a button clicked or a text was entered in an input. through actions is the only way we can change the state
reducer - simple function that gets the current state, the action that happened and decide what the next state will look like.

@ngrx/store

@ngrx/store implemented Redux as a data stream with Reactive X. Our data stream is a stream with async actions happening. and that stream of actions will be mapped to another stream of the state. Implementing Redux using RXJS fits perfectly with angular common usage of RXJS.

Translating Redux concepts to @ngrx/store terms:
- state will be defined with interfaces. the state will be divided according to our modules and every module semi state will be made from chunks where each chunk will have an interface and will be controlled by reducer.
- actions - will be classes that implement the Action interface which means they have a property of called type.
- Reducers - Our modules contain semi state where each semi state is controlled by ActionReducerMap which is an object containing many reducers, each reducer controls a chunk from our semi state. Let's try and experiment with @ngrx/store and understand how to connect it to our angular app.

Installing @ngrx/store

Let's start a new angular application.

> npx @angular/cli new ng-ngrx-store-tutorial

In this demo of ngrx/store I want to use the state as a means of transfering information between components. Our state will contain todo tasks array. that array will be an array of strings. We will have a CreateTodo component that will add a new item to the array. We will have a TodoList component that will display the list. We can understand how our state looks and by knowing how it should change we can tell we have a single action to add a todo item to the list in the state. Let's install @ngrx/store

> npm install @ngrx/store --save

Our state

When thinking redux we have to look at our app as state and state changes through actions. In my experiance it's easier to start adding redux by first defining our state. In your app folder create a folder called redux In that folder create a file called state.ts with the following interface defining our state:

export interface ITodo {
    tasks: string[]
}

export interface IState {
    todo: ITodo;
}

Notice how we are splitting our state to chunks. We have the entire state represented in the IState interface. The IState interface is combined from chunks of portion of the state, which we defined a chunk in charge of the todo tasks. If we had a part of the state dedicated to a settings portion then we would probably have another chunk described in the interface ISettings.

@ngrx/store module

Usually when using an angular package we need to add the package module to the list of imports in the module we want to use that package. Modify the file app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import {StoreModule} from '@ngrx/store';

@NgModule({
    declarations: [
        AppComponent
    ],
    imports: [
        BrowserModule,
        StoreModule.forRoot(?, ?)
    ],
    providers: [],
    bootstrap: [AppComponent]
})
export class AppModule { }

The module needs to be inserted for every module that correspondes with the state. We are breaking our angular apps to different modules so different modules will have their own part in the state and they will include the StoreModule.forFeature. for create the store module we need to supply the reducers that are in charge of the state section of this module. The second argument is an optional store configuration. We can seperate our state to multiple reducers but since our app is pretty simple there is no need to do so.

ActionReducerMap

The ActionReducerMap is an Object containing ActionReducers (often refered to as reducers in redux terms). Each reducer is in charge of a chunk from our state. Note that we can have multiple ActionReducerMap it doesn't mean we have to have one with all the chunks we defined in our state. In our case it's a simple app so we might as well have a single ActionReducerMap and a single Reducer. Let's start by creating our ActionReducer. In the redux folder create another folder called reducers. The reducer is in charge of a chunk from our state. We will create a reducer that is in charge of the todo chunk from our state. In the reducers folder create a file called: todo.ts

import { ITodo } from "../state";
import { Action, ActionReducer } from "@ngrx/store";

const initialState : ITodo = {
    tasks: []
} 

export const todoReducer : ActionReducer<ITodo> = function reducer(state: ITodo = initialState, action: Action) : ITodo {
    // TODO
    return state;
}

We will add the implementation for the reducer soon enough. For now notice that our reducer is in charge of the ITodo portion. The reducer is a good place to init the initial chunk of our state. The reason is that when @ngrx/store is initiating, it will call the reducers with a state set to undefined and expect to get the state back.

Now that we have our reducer we can create the ActionReducerMap. In the reducers folder, create a file called index.html with the following:

import { ActionReducerMap } from "@ngrx/store";
import { IState } from "../state";
import { todoReducer } from "./todo";

export const actionReducerMap : ActionReducerMap<IState> = {
    todo: todoReducer
}

our ActionReducerMap is in charge of the entire IState and setting the todo chunk to be handled by the ActionReducer (reducer) we created. Now back in our app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import {StoreModule} from '@ngrx/store';
import { actionReducerMap } from './redux/reducers';

@NgModule({
    declarations: [
        AppComponent
    ],
    imports: [
        BrowserModule,
        StoreModule.forRoot(actionReducerMap)
    ],
    providers: [],
    bootstrap: [AppComponent]
})
export class AppModule { }

We placed in the StoreModule factory method the action reducer map we created. The same process will repeat itself if we create another module and would like to connect it to the state as well. We will define an interface of the state of that module, split the state to chunk interfacesm create a reducer for each chunk and create an ActionReducerMap for the entire state of the module. We will then called StoreModule.forFeature with the action reducer map we created.

Actions

Time to create our actions which describe the change that happened in our app and will cause the state to change. @ngrx/store action, is a class that implements the Action interface. The Action interface will make us add a property called type with an action identifier. If the action needs to pass data to the reducer (like in our case we need to add a string to the list) the action will usually have another property containing the additional data, that property is usually called payload. We will create an action that adds an item to our todo list. In the redux folder, create a file called actions.ts with the following:

import {Action} from '@ngrx/store';

export class AddTask implements Action {
    static NAME = 'ADD_TASK';
    type = AddTask.NAME;

    constructor(public payload: string) {}
}

we also added a static name of the action so we can reuse it in the reducer as well. When creating the action the payload of the additional string to add to the array will be returned.

Now we need to modify the reducer to accept this action and to add the payload to the list of tasks. Modify the file todo.ts in the reducers:

import { ITodo } from "../state";
import { Action, ActionReducer } from "@ngrx/store";
import {AddTask} from '../actions';

const initialState : ITodo = {
    tasks: []
} 

export const todoReducer : ActionReducer<ITodo> = function reducer(state: ITodo = initialState, action: Action) : ITodo {
    switch(action.type) {
        case AddTask.NAME:
            const newTasks = [...state.tasks];
            newTasks.push((<AddTask>action).payload);
            return {...state, tasks: newTasks};
        default:
            return state;
    }
}

We are examining the action type and if it's equal to the AddTask type we will clone the tasks array and add a new item and clone the state with the new array. Notice that we are cloning the state and the array since they need to be immutable. It's common to also use an immutable library like immutable.js to create the state. This will enforce the programmers to keep the state immutable, but of course you can also use the method we are using here.

Connecting to our components

We would like to connect the state to angular components. We would like one component that will display the list of tasks, and one component that will add to the list of tasks. Let's start by creating our components with angular cli. In the terminal type:

> ng g c TodoList --inline-template
> ng g c CreateTodo --inline-template

Let's start with the TodoList component. We want to attach that component to a certain part of the state, and we want the component to activate change detection only when that state will change. Specifiacally we want to connect the TodoList component to the tasks string array. We can grab parts of the state we want in the component and get that part as an Observable. Getting the part of our state as an observable means we can connect it to the component template with an async pipe which will be perfect to keep the component in a change detection strategy of OnPush. Modify the todo-list.component.ts as follows:

import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { IState } from '../redux/state';
import { Observable } from 'rxjs';

@Component({
    selector: 'app-todo-list',
    template: `
        <ul>
        <li *ngFor="let item of tasks$ | async">
            {{item}}
        </li>
        </ul>
    `,
    styleUrls: ['./todo-list.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodoListComponent {
    tasks$ : Observable<string[]>;

    constructor(store : Store<IState>) { 
        this.tasks$ = store.pipe(
        select('todo', 'tasks')
        )
    }
}

We can ask from the DI to get our store. Our store is an observable so we can use the select operator that ngrx supplied us to select part of the state we need. We get that as an observable and connect it to the template with async pipe. Our entire component as well as 99% of component in a redux app can be set to OnPush state which will optimize our app performance.

Now we need to connect our action to the task create component. Modify the file create-todo.component.ts

import { Component, ChangeDetectionStrategy } from '@angular/core';
import { Store } from '@ngrx/store';
import { IState } from '../redux/state';
import { AddTask } from '../redux/actions';

@Component({
    selector: 'app-create-todo',
    template: `
        <form (ngSubmit)="addTask()">
        <div class="form-group">
            <label>New Todo:</label>
            <input name="task" type="text" [(ngModel)]="task" class="form-control" />
        </div>
        <div class="form-group">
            <button type="submit">
            Submit
            </button>
        </div>
        </form>
    `,
    styleUrls: ['./create-todo.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class CreateTodoComponent{
    task = '';
    constructor(private _store : Store<IState>) { }

    addTask = () => {
        this._store.dispatch(new AddTask(this.task));
    }
}

to call on actions we define we inject the store and call the dispatch method on the store while passing an instance of our action. Note that our template is using directives from FormModule so we will need to add the FormModule to our module. We will also need to modify the app component to include the components we just created. After doing this try and run your app and you will see how we can communicate with two components using redux and how we can keep our component in an OnPush strategy.

Debug the state

When discovering a bug in a redux connected app, we would often want to know the state that yielded the bug. It would be comfortable if we could know the state of our app without injecting the store to do so. Todo this we can connect our StoreModule with @ngrx/store-devtools. Let's first install the package, in your terminal type:

> npm install @ngrx/store-devtools --save

Now in app.module.ts add the module you just installed:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import {StoreModule} from '@ngrx/store';
import { actionReducerMap } from './redux/reducers';
import { TodoListComponent } from './todo-list/todo-list.component';
import { CreateTodoComponent } from './create-todo/create-todo.component';
import {FormsModule} from '@angular/forms';
import {StoreDevtoolsModule} from '@ngrx/store-devtools';
import { environment } from 'src/environments/environment';

@NgModule({
    declarations: [
        AppComponent,
        TodoListComponent,
        CreateTodoComponent
    ],
    imports: [
        BrowserModule,
        StoreModule.forRoot(actionReducerMap),
        FormsModule,
        StoreDevtoolsModule.instrument({
        logOnly: environment.production
        })
    ],
    providers: [],
    bootstrap: [AppComponent]
})
export class AppModule { }

Now to view the state you will need to install a browser extension. Redux Dev Tools extension.

Summary

If I could recommend an architecture for big frontend projects, Redux will be it. In my opinion it is a must for big projects and other then great structure of your app, easy maintenance and testing and easy scalable app it also improve the performance drastically.