Tallan Blog

Tallan’s Experts Share Their Knowledge on Technology, Trends and Solutions to Business Challenges

State Management with Angular & Redux

Before the chaos of 2020, I was dealing with chaos in the form of application state.  I was working on an Angular reporting project and we were using dynamic components powered by ChartJS and dealt with data managed in ag-grid.  Very quickly I realized that state was going to be a huge issue, since a single dashboard of the application would be littered with charts that would all be filtered via slicers. Ultimately a user could click then through a chart to a grid of the underlying data; shades of a from-scratch PowerBI report or visual. I had heard of the Redux pattern and how it was a game changer for Facebook’s application state (technically FB’s pattern is FLUX, but Redux is inspired by it), so I decided to do some digging, and found that it would work great for us.

The basic doctrine of the Redux pattern is that application state is managed centrally and is completely immutable.  States can only be created using pure functions from the previous state, which leads to a very predictable model of state where debugging can act similar to time traveling and allows for things such as undoing operations and persisting state.

To really learn how it works, you need to know the pieces of the puzzle:

  • Reducers – In JavaScript this is purely a function that takes arguments and creates a single result from it.  In regards to Redux, a reducer leverages pure functions to generate and return a new application state from the existing.
  • Actions – An action is an argument given to the Redux reducer which has a type and optional arguments which guide the reducer to the new state.
  • The Store – The store manages the application state tree which can only be modified by dispatching actions which are reduced to the new application state.

That’s it. There are really only those pieces that make it work.

Now what does it actually look like to get this working in an Angular application?

In my sample project I’m working with Angular 8.2.13 (9+ is available at this time).  The packages I am using are as follows (from package.json):

Note: this is just a sample app and not the reporting engine

  "dependencies": {
    "@angular-redux/store": "10.0.0", // this is the redux heart
    "@angular/animations": "~8.2.13",
    "@angular/common": "~8.2.13",
    "@angular/compiler": "~8.2.13",
    "@angular/core": "~8.2.13",
    "@angular/forms": "~8.2.13",
    "@angular/platform-browser": "~8.2.13",
    "@angular/platform-browser-dynamic": "~8.2.13",
    "@angular/router": "~8.2.13",
    "redux": "4.0.1",
    "redux-devtools-extension": "2.13.8",
    "redux-logger": "3.0.6",
    "rxjs": "~6.4.0",
    "tassign": "1.0.0",
    "tslib": "^1.10.0",
    "zone.js": "~0.9.1"
  },
 

I then created a store module, that has its constructor initialize the NgRedux store. This is not necessary and can be done in the regular app module, but for the sake of demonstration I kept them separated.

import { NgModule } from '@angular/core';
import { NgReduxModule, NgRedux, DevToolsExtension } from '@angular-redux/store';
import { createLogger } from 'redux-logger';

@NgModule({
  imports: [NgReduxModule],
})
export class StoreModule {
  constructor(store: NgRedux<IAppState>,
    devTools: DevToolsExtension
  ) {
    store.configureStore(
        reducer,
        INITIAL_APP_STATE,
        [ createLogger()],
        devTools.isEnabled() ? [ devTools.enhancer() ] : []
        );
  }
}
 

Let’s walk through the arguments to configuring the store. The first “reducer” is the main reducer. I say main, because there can be more than one, but they should be targeting different state interfaces. This is likely unclear, so let’s take a peek at the second argument and we’ll circle back. The second argument is a reference to a default/initial application state. In the case of my demo app, it looks like this:

export interface IAppState{
    score: any,
    scoreHistory: number[],
    activeFlag: boolean,
    gameState: IGameState  // <-- substate
}
export interface IGameState{
    possession: string,
    yardage: number,
    down: number,
    fieldGoalAttempt: boolean
}
export const INITIAL_APP_STATE: IAppState = {
    score: {
        chiefs: 0,
        forty9ers: 0
    },
    gameState: {
        possession: 'Chiefs',
        yardage: 70,
        down: 1,
        fieldGoalAttempt: false
    },
    scoreHistory: [],
    activeFlag: false
}

At this point, you may have noticed that my demo app was inspired by the last Super Bowl. I figured a football simulator had enough moving parts to make a compelling demonstration (it was never completely finished though). My state actually has a substate managed with a IGameState, which could be managed with a second reducer targeting itself only, that is the reason you might have more than one reducer defined for your application. Now that we have the state, let’s circle back to the reducer and I’ll show you the one I defined. (I actually reduced my reducer to only one case in the switch statement as it is quite lengthy).

export function reducer(state: IAppState, action: any) : IAppState{
    switch(action.type){  // <-- remember an action has a type
        case(APPLICATION_ACTIONS.RUN): // based on type we do work
            if(state.gameState.down === 4 && action.distance <= 0){
                return tassign(state, {
                    gameState: tassign(state.gameState, {
                        down: 1,
                        possession: togglePossession(state.gameState.possession),
                        yardage: 100 - (state.gameState.yardage - action.distance)
                    })
                })
            }
            else if(state.gameState.yardage - action.distance >= 100){
                return tassign(state, {
                    score: tassign(state.score, {
                        chiefs: state.gameState.possession === 'Chiefs' ? state.score.chiefs + 1 : state.score.chiefs,
                        forty9ers: state.gameState.possession === 'Forty9ers' ? state.score.forty9ers + 1 : state.score.forty9ers,
                    }),
                    gameState: tassign(state.gameState, {
                        down: 1,
                        possession: togglePossession(state.gameState.possession),
                        yardage: 100 - (state.gameState.yardage - action.distance)
                    })
                })
            }
            else if(state.gameState.yardage - action.distance <= 0){
                return tassign(state, {
                    score: tassign(state.score, {
                        chiefs: state.gameState.possession === 'Chiefs' ? state.score.chiefs + 6 : state.score.chiefs,
                        forty9ers: state.gameState.possession === 'Forty9ers' ? state.score.forty9ers + 6 : state.score.forty9ers,
                    }),
                    gameState: tassign(state.gameState, {
                        fieldGoalAttempt: true,
                        yardage: 30
                    })
                })

            }
            else{
                return tassign(state, {
                    gameState: tassign(state.gameState, {
                        yardage: state.gameState.yardage - action.distance,
                        down: action.distance <= 0 ? state.gameState.down + 1 : 1
                    })
                })
            }
    }
    return state;
}

This is a rough rule set for a run action in my mock football simulator, which is really rudimentary. Moving on with the original args, the third argument in the constructor is an array of middlewares that may want to be used. The package redux-logger offers a plug-in logger, which we have defined. The last argument is an array of Redux enhancers to which we have defined a conditional checking for Redux devtools.

store.configureStore(
        reducer,
        INITIAL_APP_STATE,
        [ createLogger()], // <-- Logger middleware
        devTools.isEnabled() ? [ devTools.enhancer() ] : [] // <-- enhancers
        );

 

Ok, so that’s it!

Let’s look at the app (warning, it’s ugly).  See below, that the display shows a score of 0,0 and Chiefs have 1st down possession at the 70 yard line.  This matches to the initial app state.  The components are subscribed to the store and get these values asynchronously as the state is updated.InitialAnother benefit of using Redux is the fact that since all of the components are generally subscribed to the store, there are no inputs or outputs required to pass data around.  Below is the html for the app.component:

<div class="row">
  <div class="col-12">
    <app-score-board></app-score-board>
    <app-field></app-field>
  </div>
</div>

Within the apps there is a @select syntax used to subscribe to the store data:

import { Component, Input, OnInit } from '@angular/core';
import { select } from '@angular-redux/store';
import { IAppState, StoreModule } from 'src/app/store/store.module';

@Component({
  selector: 'app-score-board',
  templateUrl: './score-board.component.html',
  styleUrls: ['./score-board.component.scss']
})
export class ScoreBoardComponent {
  // SEE BELOW - THESE ARE SUBSCRIPTIONS TO THE STORE
  @select((store:IAppState) => store.score.chiefs) chiefs: number;
  @select((store:IAppState) => store.score.forty9ers) forty9ers: number;

  constructor() {
  }
}

The html just has the value loaded via async pipe:

<div class="col-6 bg-warning">
   <h1 class="display-3">Chiefs</h1>
   <p class="display-4">{{ chiefs | async }}</p>
</div>
<div class="col-6 bg-info">
   <h1 class="display-3">49ers</h1>
   <p class="display-4">{{ forty9ers | async }}</p>
</div>

If a component wants to perform an update to the application state, it needs to dispatch the action to the store:

  runIt(){
    if(this.randomBoolean()){
      var action = {type: APPLICATION_ACTIONS.RUN, distance: this.randomRun() };
    }
    else{
      var action = {type: APPLICATION_ACTIONS.RUN, distance: this.randomRunOrSack() };
    }
    this.store.dispatch(action); // <- the run action is dispatched
  }

The distance argument is consumed by the reducer and then based on other arguments in the state, a new state is generated. Given the initial state, with the yardage and down count, the result will be the else condition shown in the reducer:

return tassign(state, {
   gameState: tassign(state.gameState, {
      yardage: state.gameState.yardage - action.distance,
      down: action.distance <= 10 ? state.gameState.down + 1 : 1
   })
})

Seen above, there are two references to tassign. Tassign is a package that effectively binds a type to the operation Object.assign, so we get strongly typed IntelliSense preventing modification of the state into having invalid properties, and it also allows the update of only properties needing changes. So, above the store is updating the yardage to match the existing yardage minus the action distance, and then also updating the down count.

Let’s take a look at the app in operation and see how the tools come into play.
footballSo, all that happened was that I chose to run the ball over and over again, and luckily scored a touch down.  As you can see, the app updated the line of scrimmage, down count, and score board all through the store, and all that was happening was the repeated behavior seen above, namely dispatching a run action.

Because the logger was configured, we can open the browser and see all of the changes to state incrementally logged.

console

 

Better than this for debugging are the Redux DevTools.  These effectively let you as a developer time travel through state.  Looking at the gif below (depending on screen size, it may be blurry) you will see the dev tools console open on the left which displays the state changes as they occur with the type of action listed next to it.  You have the ability to replay the execution on demand, as well as the ability to jump to a state, or even skip an action execution and then rerun the whole execution sequence.  On the right hand side you can also interrogate the application state and see a diff from the previous state into the new state.  This is an incredibly useful debugging tool if your app is demonstrating erratic behavior.

devtools

Alright, and with that I leave you hopefully a little more knowledgeable about Redux.

tl;dr; Redux is a state management design pattern that has some sweet tooling.

 

Share this post:

No comments

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

\\\