Introduction to the NGRX Suite, Part I

Jim Armstrong
ngconf
Published in
17 min readOct 6, 2020
NgRx state management, courtesy https://ngrx.io/guide/store

An organized introduction to @ngrx/store, @ngrx/effects, and @ngrx/entity

Introduction

This article is intended for relatively new Angular developers who are just starting to work with an organized store in their applications. The NgRx suite is one of the most popular frameworks for building reactive Angular applications. The toolset does, however, come with a learning curve, especially for those not previously familiar with concepts such as Redux.

In talking with new Angular developers, a common communication is frustration with moving from online tutorials such as counters and TODO apps to actual applications. This article attempts to bridge that gap with an organized and phased introduction to @ngrx/store, @ngrx/effects, and @ngrx/entity.

Instead of discussing all three tools in one massive tutorial, the application in this series is broken into four parts. This application is an extension of a quaternion calculator that I have frequently used as a ‘Hello World’ project for testing languages and frameworks. This calculator has been extended to more closely resemble a practical application that might be developed for an EdTech client.

Now, if the term quaternions sounds mathematical and scary, don’t worry. If you have read any of my past articles, then you know we have a tried and true technique for dealing with pesky math formulas. Here it goes …

blah, blah … math … blah, blah … quaternions … blah, blah … API.

Ah, there. We’re done :). Any math pertaining to quaternions is performed by my Typescript Math Toolkit Quaternion class. The Typescript Math Toolkit is a private library developed for my clients, but many parts of it have been open-sourced.

All you need in order to understand this tutorial series is:

1 — Some prior exposure to @ngrx/store; at least a counter or TODO app (see the docs at https://ngrx.io/docs, for example).

2 — Ability to work with a data structure containing four numbers.

3 — Ability to call an API for add, subtract, multiply, and divide.

4 — Exposure to basic Angular concepts and routing, including feature modules and lazy-loading.

<aside>
While quaternions were conceived as an extension to complex numbers, they have several practical applications, most notably in the area of navigation. A quaternion may be interpreted as a vector in three-dimensional (Euclidean) space along with a rotation about that vector.
This use of quaternions was first applied to resolution of the so-called Euler-angle singularity; a situation where the formula for motion of an object exhibits a singularity at a vertical angle of attack. This situation is sometimes called gimbal lock. Equations of motion developed using quaternions exhibit no such issues. In reality, the Euler-angle equations are NOT singular; they are indeterminate. Both the numerator and denominator approach zero at a vertical angle of attack. L'Hopital's rule is necessary to evaluate the equations at this input value. Such an approach is cumbersome, however, and quaternions provide a cleaner and more efficient solution. Quaternions are also used in inverse kinematics (IK) to model the motion of bone chains. Quaternions avoid 'breaking' or 'popping' that was prevalent in early 3D software packages that resolved IK motion using Euler-angle models.
</aside>

The Application

The application covered in this series is an abbreviated learning module involving quaternions and quaternion arithmetic. It consists of a login screen, a calculator that allows students to practice quaternion-arithmetic formulas, and an assessment test. An actual application might also include reading material on the topic, but that has been omitted for brevity. The general application flow is

1 — Login.

2 — Present student with the calculator for practice and option to take assessment test. The calculator is always displayed while the assessment test is optional.

3 — A test is scored after completion, and then results are displayed to student followed by sending the scored test to a server.

The tutorial series is divided into four parts, which might correspond to application sprints in practice:

Part I: Construct the global store by features using @ngrx/store and implement the calculator. Login and test views are placeholders.

Part II: Complete the test view using @ngrx/effects for retrieval of the assessment test and communication of scored results back to a server. Service calls are simulated using a mock back end.

Part III: Use @ngrx/entity to model and work with test data in the application.

Part IV: Implement the login screen using simple authentication and illustrate concepts such as redirect url. This further introduces how to use @ngrx/store in an environment similar to that you might encounter in actual work.

At present, stakeholders have prescribed that the student will always log in before being directed to the calculator practice view. As seasoned developers, we know that will change, so our plan is to work on the calculator first as it is the most complex view. The calculator also addresses the most complex slice of the global store.

Before continuing, you may wish to follow along or fork the Github for the application (in its Part I state).

Models

Before we can construct a global store, it is necessary to understand models required by each feature in the application. Following is an outline of each feature’s data requirements as initially presented. Only the calculator requirement is believed to be solid as of this article.

User Model: first name, last name, class id, student id, and whether or not the student is authenticated to use this application.

Calculator Model: Quaternion and calculator models.

Test Model: Test id, string question, quaternion values for the correct answer and the student’s input.

The application also has a requirement that once a test has begun, the student may not interact with the calculator.

User model

The working User model at this point is

export interface User
{
first: string;

last: string;

classID: string;

studentID: string;

authorized: boolean;
}

There is also ‘talk’ about possibly echoing the user’s name back to them on a successful answer, i.e. ‘That’s correct. Great job, Sandeep!’ For present, we choose to make the entire user model a single slice of the global store.

Quaternion Model

For tutorial purposes, a quaternion consists of four numbers, w, i, j, and k. The student understands these to be the real part, and the amounts of the vector along the i, j, and k axes, respectively. As developers, we don’t care. It’s just four numbers, always provided in a pre-defined order. Based on past applications, I have supplied a class to organize this data, named after an infamous Star Trek TNG character :)

/src/app/shared/definitions/Q.ts

Calculator Model

The calculator consists of two input quaternions, a result quaternion, operation buttons for add/subtract/multiply/divide, and to/from memory buttons.

The state of the entire calculator is represented in /src/app/shared/definitions/QCalc.ts

Test Model

The test section of the application is only a placeholder in Part I of this series. The test is not formally modeled at this time.

After examining these models, it seems that the application store consists of three slices, user, calculator, and test, where the latter slice is optional as the student is not required to take the test until they are ready.

These slices are currently represented in /src/app/shared/calculator-state.ts

import { User  } from './definitions/user';
import { QCalc } from './definitions/QCalc';
export interface CalcState
{
user: User;
calc: QCalc;
test?: any;
}

Features

The application divides nicely into three views or features, namely login, practice with calculator, and assessment test. These can each be represented by a feature module in the application. Each feature also contributes something to the global store.

The login screen contributes the user slice. The ‘practice with calculator’ view contributes the QCalc or calculator slice of the store. The assessment test contributes the test slice of the global store.

A feature of @ngrx/store version 10 is that the global store need not be defined in its entirety in the main app module. The store may be dynamically constructed as features are loaded into the application.

The /src/app/features folder contains a single folder for each feature module of the application. Before deconstructing each feature, let’s look at the high-level application structure in /src/app/app.module.ts,

Notice that unlike other @ngrx/store tutorials you may have seen in the past, the global store is empty,

StoreModule.forRoot({}),

In past examples of using @ngrx/store for just the quaternion calculator, I defined the reducers for each slice,

import { QInputs } from "./QInputs";
import { QMemory } from "./QMemory";

export interface CalcState
{
inputs: QInputs;

memory: QMemory;
}

import { ActionReducerMap } from '@ngrx/store';
import {inputReducer, memoryReducer} from "../reducers/quaternion.reducers";

export const quaternionCalcReducers: ActionReducerMap<CalcState> =
{
inputs: inputReducer,
memory: memoryReducer
};

and then imported quaternionCalcReducers into the main app module, followed by

@NgModule({
declarations: APP_DECLARATIONS,
imports: [
PLATFORM_IMPORTS,
MATERIAL_IMPORTS,
StoreModule.forRoot(quaternionCalcReducers)
],
providers: APP_SERVICES,
bootstrap: [AppComponent]
})

The current application begins with an empty store. The application’s features build up the remainder of the store as they are loaded.

And, on the subject of loading, here is the main app routing module,

Part I of this tutorial simulates a realistic situation where we don’t have a full, signed-off set of specifications for login and we may not even have complete designs. Login is deferred until a later sprint and the application currently displays the calculator by default. Note that the calculator is always available to the student when the application loads.

The test is always optional, so the test module is lazy-loaded.

Our deconstruction begins with the login feature.

Login Feature (/src/app/features/login)

This folder contains a login-page folder for the Angular Version 10 login component as well as the following files:

  • login.actions.ts (actions for the login feature)
  • login.module.ts (Angular feature model for login)
  • login.reducer.ts (reducer for the login feature)

Unlike applications or tutorials you may have worked on in the past, a feature module may now contain store information, component, and routing definitions.

My personal preference is to consider development in the order of actions, reducers, and then module definition.

Login actions

These actions are specified in /src/app/features/login-page/login.actions.ts,

import {
createAction,
props
} from '@ngrx/store';

import { User } from '../../shared/definitions/user';

export const Q_AUTH_USER = createAction(
'[Calc] Authenticate User'
);

export const Q_USER_AUTHENTICATED = createAction(
'[Calc] User Authenticated',
props<{user: User}>()
);

The expectation is that the username/password input at login are to be sent to an authentication service. That service returns a User object, part of which is a boolean to indicate whether or not that specific login is authorized for the application.

If you are not used to seeing props as shown above, this is the @ngrx/store version 10 mechanism to specify metadata (payloads in the past) to help process the action. This approach provides better type safety, which I can appreciate as an absent-minded mathematician who has messed up a few payloads in my time :)

Login reducers

Reducers modify the global store in response to specific actions and payloads. Since the global store is constructed feature-by-feature, each feature module contains a feature key that is used to uniquely identify the slice of the global store covered by that feature.

The reducer file also defines an initial state for its slice of the store. This is illustrated in the very simple reducer from /src/app/features/login-page/login.reducer.ts,

import {
createReducer,
on
} from '@ngrx/store';

import * as LoginActions from './login.actions';

import { User } from '../../shared/definitions/user';

const initialLoginState: User = {
first: '',
last: '',
classID: '101',
studentID: '007',
authorized: true
};

// Feature key
export const userFeatureKey = 'user';

export const loginReducer = createReducer(
initialLoginState,

on( LoginActions.Q_AUTHENTICATE_USER, (state, {user}) => ({...state, user}) ),
);

Spread operators may be convenient, but always be a bit cautious about frequent use of shallow copies, especially when Typescript classes and more complex objects are involved. You will note that all my Typescript model classes contain clone() methods and frequent cloning is performed before payloads are even sent to a reducer. This can be helpful for situations where one developer works on a component and another works on reducers. Sloppy reducers can give rise to the infamous ‘can not modify private property’ error in an NgRx application.

Login feature module

The login component is eagerly loaded. The login route is already associated with a component in the main app routing module. The login feature module defines the slice of the global store that is created when the login module is loaded.

/src/app/features/login-page/login.module.ts

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

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

import * as fromLogin from './login.reducer';

@NgModule({
imports:
[
StoreModule.forFeature(fromLogin.userFeatureKey, fromLogin.loginReducer),
],
exports: []
})
export class LoginModule {}

Since LoginModule is imported into the main app module, the user slice of the global store is defined as soon as the application loads.

The test module, however, is lazy-loaded, so its implementation is slightly more involved.

Test Feature (/src/app/features/test)

This folder contains the test folder for the Angular component files as well as feature-related files. As with login, the feature-specific files are

  • test.actions.ts (actions for the test feature)
  • test.module.ts (Angular feature model for test)
  • test.reducer.ts (reducer for the login feature)

And, as before, these are deconstructed in the order, actions, reducers, and then feature module.

Test Actions

As of Part I of this tutorial, we anticipate four test actions,

1 — Request a list of test questions from a server (Q_GET_TEST)

2 — Indicate that the test has begun (Q_BEGIN_TEST)

3 — Send a collection of scored test results back to the server (Q_SCORE_TEST)

4 — Send test results back to the server (Q_SEND_TEST_RESULTS)

The second action is needed to ensure that the calculator can not be used once the test begins.

/src/app/features/test/test.actions.ts

import {
createAction,
props
} from '@ngrx/store';

// Feature key
export const textFeatureKey = 'test';

export const Q_GET_TEST = createAction(
'[Calc] Get Test'
);

export const Q_BEGIN_TEST = createAction(
'[Calc] Begin Test',
props<{startTime: Date}>()
);

export const Q_SCORE_TEST = createAction(
'[Calc] Score Test',
props<{results: Array<any>}>()
);

export const Q_SEND_TEST_RESULTS = createAction(
'[Calc] Send Test Results',
props<{endTime: Date, results: Array<any>}>()
);

A feature key is again used as a unique identifier for the test slice of the global store. Part I of this tutorial simulates a situation where we have not been given the model for a collection of test questions. Nor do we understand how to extend that model to include scored results. Typings applied to the payload for the final two actions are simply placeholders.

<hint>
Stories typically have unique identifiers in tracking systems. Consider using the tracking id as part of the action name. In the case of Pivotal Tracker, for example, 'ADD [PT 10472002]'. This string contains the operation, i.e. 'ADD', along with the Pivotal Tracker ID for the story. This allows other developers to quickly relate actions to application requirements.
</hint>

Test Reducers

The current test reducer and initial test state are placeholders for Part I of this tutorial.

/src/app/features/test/test.reducer.ts

import * as TestActions from './test.actions';

import {
createReducer,
on
} from '@ngrx/store';

// At Part I, we don't yet know the model for a test question
const initialTestState: {test: Array<string>} = {
test: new Array<any>()
};

// Feature key
export const testFeatureKey = 'test';

const onGetTest = on (TestActions.Q_GET_TEST, (state) => {
// placeholder - currently does nothing
return { state };
});

export const testReducer = createReducer(
initialTestState,
onGetTest
);

Test Module

The test module defines routes and adds the test slice to the global store,

/src/app/features/test/test.module.ts

import { NgModule     } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
RouterModule,
Routes
} from '@angular/router';

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

import * as fromTest from './test.reducer';

import { TestComponent } from './test/test.component';

import { AuthGuard } from '../../shared/guards/auth-guard';

const routes: Routes = [
{ path: '', component: TestComponent, canActivate: [AuthGuard] }
];

@NgModule({
declarations: [
TestComponent
],
imports:
[
CommonModule,
StoreModule.forFeature(fromTest.testFeatureKey, fromTest.testReducer),
RouterModule.forChild(routes)
],
providers: [AuthGuard],
exports: [
]
})
export class TestModule {}

Notice that a route guard has been added to the default child route. This guard ensures that the test route may not be directly requested unless the user is currently authorized. The guard will be fully implemented in part IV of this tutorial. The current implementation simply hardcodes an authenticated flag, so that any user is considered authorized.

Calculator Feature (/src/app/features/quaternion-calculator)

The calculator is the main focus of Part I of this tutorial, so its action list is complete,

/src/app/features/quaternion-calculator/calculator.actions.ts

import {
createAction,
props
} from '@ngrx/store';


import { Q } from '../../shared/definitions/Q';

// Actions
export const Q_UPDATE = createAction(
'[Calc] Update',
props<{id: string, q: Q}>()
);

export const Q_ADD = createAction(
'[Calc] Add',
props<{q1: Q, q2: Q}>()
);

export const Q_SUBTRACT = createAction(
'[Calc] Subtract',
props<{q1: Q, q2: Q}>()
);

export const Q_MULTIPLY = createAction(
'[Calc] Multiply',
props<{q1: Q, q2: Q}>()
);

export const Q_DIVIDE = createAction(
'[Calc] Divide',
props<{q1: Q, q2: Q}>()
);

export const Q_CLEAR = createAction(
'[Calc] Clear',
);

export const TO_MEMORY = createAction(
'[Calc] To_Memory',
props<{q: Q, id: string}>()
);

export const FROM_MEMORY = createAction(
'[Calc] From_Memory',
props<{id: string}>()
);

Note that all payloads involving quaternions use the generic ‘Q’ class. This allows the reducer the greatest flexibility in implementing calculator operations. Before we look at the reducer, though, recall that the Typescript Math Toookit TSMT$Quaternion class is used to implement all quaternion arithmetic. In the future, though, a different class (or collection of pure functions) might be used.

With future changes in mind, the Adapter Pattern is applied to create an intermediary between the generic ‘Q’ structure and the code responsible for quaternion arithmetic. This helper class is located in /src/app/shared/libs/QCalculations.ts

This class currently uses TSMT$Quaternion for quaternion arithmetic. If another library is used in the future, it is not necessary to change reducer code; only the helper class need be modified. This helper or adapter class can also have its own set of tests, which serves to strengthen tests already present for reducers.

Now, we can deconstruct the calculator reducers. The createReducer() method from @ngrx/store seems so simple with one-line reducers in a scoreboard or counter application. The quaternion calculator is different in that reduction for each calculator operation is more involved.

Let’s look at one action, calculator addition. The second argument to the @ngrx/store on() method is the combination of prior store and payload. The payload shape is described in the action, so examine the action and reducer side-by-side:

export const Q_ADD = createAction(
'[Calc] Add',
props<{q1: Q, q2: Q}>()
);
.
.
.
const onAdd = on (CalculatorActions.Q_ADD, (state, {q1, q2}) => {
const calculator: CalcState = state as CalcState;

const q: Q = QCalculations.add(q1, q2);

return { ...calculator.user, calc: calcFatory(calculator.calc, q1, q2, q) };
});

Other calculation computations are handled in a similar manner. Note that an id is involved in moving quaternion data to and from calculator memory and this id is specified in the quaternion calculator template,

/src/app/features/quaternion-calculator/calculator/calculator.component.html

.
.
.
<div class="card-center">
<app-quaternion id="q1" [inputDisabled]="inputDisabled" (qChanged)="onQuaternionChanged($event)"></app-quaternion>
</div>
<app-memory id="Q_1" (memTo)="onToMemory($event)" (memFrom)="onFromMemory($event)"></app-memory>
.
.
.

Recall that the QCalc class is used to represent the calculator slice of the global store, so initial calculator state is simply a new instance of this class,

const initialCalcState: {calc: QCalc} = {
calc: new QCalc()
};

and, the reducer for all calculator actions is defined at the end of the process,

export const calculatorReducer = createReducer(
initialCalcState,
onUpdate,
onAdd,
onSubtract,
onMultiply,
onDivide,
onToMemory,
onFromMemory,
onClear
);

The calculator route is eagerly loaded and already specified in the main app routing module, so the calculator module only handles adding the calculator section or slice to the global store,

/src/app/features/quaternion-calculator/calculator.module.ts

.
.
.

@NgModule({
declarations: [
CalculatorComponent,
QuaternionComponent,
MemoryComponent,
ResultComponent,
],
imports:
[
CommonModule,
FormsModule,
MAT_IMPORTS,
StoreModule.forFeature(fromCalculator.calculatorFeatureKey, fromCalculator.calculatorReducer),
],
exports: [
]
})
export class CalculatorModule {}

This process seems intimidating at first, but only if you try to absorb everything at one time. I personally like the build-the-store-by-feature approach illustrated above, as it’s very intuitive. Remember the order actions, reducers, module, and try working on just one action and one reducer function at a time. That’s exactly what I did when preparing this tutorial. I worked on the ADD action first. Then, I implemented SUBTRACT. I noticed some repeated code and made the reducers more DRY. Then, the remainder of the calculator reducers came together in short order.

Store Selection

Components query the store (or some subset) and generally reflect those values directly into the component’s template. This application is different in that some components follow that exact model while others such as the calculator maintain an internal copy of the calc slice of the store. That component’s template does not directly reflect any of the calc values. It maintains a constant sync with the ‘q1’ and ‘q2’ input quaternions in order to dispatch copies of them as payloads when the user clicks on one of the operations (add/subtract/multiply/divide).

@ngrx/store provides the ability to direct-select a named slice from the store and assign the result to an Observable. This feature is illustrated in the counter app in the @ngrx/store docs.

Store selectors may also be created, which direct-select exact slices of the store or subsets of those slices. This process is illustrated in the calculator reducer file, /src/app/features/quaternion-calculator/calculator.reducer.ts,

.
.
.
export const getCalcState = createFeatureSelector<CalcState>(calculatorFeatureKey);

export const getCalculator = createSelector(
getCalcState,
(state: CalcState) => state ? state.calc : null
);

// Select result quaternion values - combine these as an exercise
export const getResultW = createSelector(
getCalcState,
(state: CalcState) => state ? (state.calc.result ? state.calc.result.w : null) : null
);

export const getResultI = createSelector(
getCalcState,
(state: CalcState) => state ? (state.calc.result ? state.calc.result.i : null) : null
);

export const getResultJ = createSelector(
getCalcState,
(state: CalcState) => state ? (state.calc.result ? state.calc.result.j : null) : null
);

export const getResultK = createSelector(
getCalcState,
(state: CalcState) => state ? (state.calc.result ? state.calc.result.k : null) : null
);

One selector fetches the calc state of the global store while the remaining four selectors query the individual values of the result quaternion.

A classic subscription model is used to handle updates from the store inside the calculator component,

/src/app/features/quaternion-calculator/calculator/calculator.component.ts

protected _calc$: Subject<boolean>;
.
.
.
this._store.pipe(
select(getCalculator),
takeUntil(this._calc$)
)
.subscribe( calc => this.__onCalcChanged(calc));

The __onCalcChanged() method simply syncs the class variable with the store,

protected __onCalcChanged(calc: QCalc): void
{
if (calc) {
this._qCalc = calc.clone();
}
}

and the unsubscribe is handled in the on-destroy lifecycle hander,

public ngOnDestroy(): void
{
this._calc$.next(true);
this._calc$.complete();
}

Next, look at the result quaternion code in /src/app/shared/components/result/result.component.ts

The result quaternion values [w, i, j, k] are directly reflected in the template and can be easily updated with the just-created selectors and an async pipe.

.
.
.
import {
getResultW,
getResultI,
getResultJ,
getResultK
} from '../../../features/quaternion-calculator/calculator.reducer';

@Component({
selector: 'app-result',

templateUrl: './result.component.html',

styleUrls: ['./result.component.scss']
})
export class ResultComponent
{
// Observables of quaternion values that are directly reflected in the template
public w$: Observable<number>;
public i$: Observable<number>;
public j$: Observable<number>;
public k$: Observable<number>;

constructor(protected _store: Store<CalcState>)
{
this.w$ = this._store.pipe( select(getResultW) );
this.i$ = this._store.pipe( select(getResultI) );
this.j$ = this._store.pipe( select(getResultJ) );
this.k$ = this._store.pipe( select(getResultK) );
}
}

/src/app/shared/components/result/result.component.html,

<div>
<mat-form-field class="qInput">
<input matInput type="number" value="{{w$ | async}}" readonly />
</mat-form-field>

<mat-form-field class="qInput qSpaceLeft">
<input matInput type="number" value="{{i$ | async}}" readonly />
</mat-form-field>

<mat-form-field class="qInput qSpaceLeft">
<input matInput type="number" value="{{j$ | async}}" readonly />
</mat-form-field>

<mat-form-field class="qInput qSpaceLeft">
<input matInput type="number" value="{{k$ | async}}" readonly />
</mat-form-field>
</div>

Result

This is the initial view for Part I after building the application.

Quaternion Application Initial View

Now, if you were expecting great design from a mathematician, then you probably deserve to be disappointed :)

Experiment with quaternion arithmetic and have fun. Be warned, however, multiplication and division are not what you might expect.

Summary

Applications are rarely built all at once. They are often created small sections at a time (usually in organized sprints). Not everything will be defined in full detail at the onset of a project, so the global store may evolve over time. I hope this tutorial series introduces the NgRx suite in a manner that is less like other tutorials and more like how you would use the framework in a complete application.

In Part II, we receive the test definition from the back-end team and a proposal for a set of service calls to implement the test view. We will mock a back end using an HTTP Interceptor and fill out the test slice of the global store. @ngrx/effects will be used to handle service interactions.

I hope you found something helpful from this tutorial and best of luck with your Angular efforts!

ng-conf: The Musical is coming

ng-conf: The Musical is a two-day conference from the ng-conf folks coming on April 22nd & 23rd, 2021. Check it out at ng-conf.org

--

--

Jim Armstrong
ngconf
Editor for

Jim Armstrong is an applied mathematician who began his career writing assembly-language math libraries for supercomputers. He now works on FE apps in Angular.