Angular: 2 Simple Examples to understand how NgRx works- Part-I

Angular&NodeEnthusiast
13 min readDec 19, 2024

--

NgRx is a huge and a powerful library for managing state in an angular app. In this 2-part series, will walk you through 2 simple examples which will help you understand how the different elements of the ngrx library work together: State, Selectors,Actions,Reducer and Effects.

What are the possible types of state you need to manage in your angular app ?

=>Persisted state from the backend.

=>URL state

=>Client State

In this story, we will see, how State, Selectors, Actions and Reducers work together with Components and Services to manage the backend and client state. We will not be looking at router or url state in this series.

We will discuss Effects in Part-II of this series.

I. Configuration

Before we begin, we just need to install the below npm package. Since I am using Angular 16, I have installed the compatible version of @ngrx/store.

ng add @ngrx/store@16

Lets update the AppModule, to import the StoreModule from @ngrx/store package we just installed. The StoreModule.forRoot() registers the global providers for the application like the Store class, which we will frequently inject into components and/or services to dispatch actions and also select the desired slice of the application state.

StoreModule.forRoot({}) means that we are setting the root state in the application state to an empty object {}.

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

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

We can add root state eagerly to the global application state or we can also add feature state lazily to the same application state. Instead of StoreModule.forRoot(), we use StoreModule.forFeature() to add feature state.

The application state is 1 large object. Both root state and feature state can be used to add additional keys-value pair in that 1 big state object. The key is a unique string to identify a slice of the state. The value corresponding to this key could be be a primitive type such as a number, or even a more complex object with multiple properties.

In the Part-II of this series, we will check examples to add feature state.

II. Counter Example

You might find this example in the ngrx documentation too. I have included this example here as well because its simple and its easier to explain more in detail with a simple example. In the 2nd part of this series, I have provided a more relatable example.

I can increment, decrement and reset the counter. By default, clicking on the increment/decrement button, will increment/decrement by 1 only. For the increment function, we have provided an additional textbox to enter the number by which the counter should be increment.

Below is a short demo of the same.

III. Workflow

Before we get into the code, let me brief you on how the 4 elements: State, Selectors, Actions and Reducers will work with an angular component. In the below workflow diagram, I have removed all the irrelevant elements.

Store is nothing but a state container and Selectors are functions that can be used to obtain the desired slice of state from the Store.

The Component will use Selectors to extract the desired state slice from the Store and also listen to any changes in the desired slice of State in the Store and update the template.

How does the state change occur ? In the absence of ngrx, the component will update the value of the properties on the occurrence of any event or as requirement demand.

With ngrx, the component will not perform any updates. Instead the component will dispatch an Action, whenever a state change is required. Dispatching an action is similar to dispatching an event. Just like we have event listeners that execute some function, when an event occurs. Similarly, we have a Reducer which is a function that listens to all dispatched actions but only executes for the dispatched actions relevant to the particular Reducer. The Reducer performs the required transitions for the desired state slice in the Store.

If the application state has single/multiple slices, how does the Reducer know, for which state slice, should the transition be applied to ?

If you recall, we discussed adding additional key-value pairs to the StoreModule.forRoot() or StoreModule.forFeature() to add additional root or feature state slices to the application state(which is initially just an empty object {}).

Please don’t confuse between the key-value pair which represents how the state slice looks like and the key-value pair added to the StoreModule.forRoot() or StoreModule.forFeature().

The key-value pair added to the StoreModule.forRoot() or StoreModule.forFeature() represents the mapping between the state slice and reducer function registered to manage the transitions in that state slice. In the absence of this mapping, the Reducer function will not execute. The key is a unique string to identify the state slice and the value is the reducer function.

On the other hand, in the key-value pair which tells us how the state slice looks like, the key is again the same unique string to identify the state slice, but the value to this key could be be a primitive type such as a number, or even a more complex object with multiple properties.

As we progress, the difference will become more prominent.

IV. Implementation

Below is the project structure. We are not including any feature modules in this example. Feature modules will be discussed in the Part-II. The actions, selector and reducer functions are defined within the state folder.

  1. The first step is to finalize the shape of state slice, we are going to add to the application state. You define the shape of the state according to what you are capturing. Here we are talking about deciding how the state slice will look like.

As we already know that the application state is a big object. Adding slices to this object is like adding key-value pairs to this object.The key is a unique name you are giving to the state slice and the value corresponding to this key could be be a simple type such as a number, or even a more complex object with multiple properties.

Below 2 statements in the counter.reducers.ts file give us an idea about the shape of the state slice. The other contents of this file will be discussed later in the story.

let initialState:number=0;
export const stateSliceKey:string="myCountState";

It is essential to give this state slice a unique name, which we will later use to map with a reducer function in RootModule. The name of this state slice is “myCountState”. I have stored the name in a constant stateSliceKey and exported it because it is required in multiple files.

Since the counter displayed in the template is nothing but a number, value corresponding to the key “myCountState” is a number. In the 2nd part of this series, we will take a complex object and demonstrate how the shape of the state can be defined.

initialState is a constant which contains the default value of the state slice.

At this point, our application state has the below structure. It is an object with 1 state slice. As we know that each state slice is a key-value pair. The key of this slice is “myCountState”. The value for this key is a number, which can be incremented/decremented/reset.

{"myCountState":0}

2. Lets now check the AppComponent Template. This is exactly what you saw in the screenshot in the beginning of the story.

<h2>Basic Counter</h2>

<p>Count: {{currentCount$|async}}</p>

<input [(ngModel)]=”incrementBy” type=”number” placeholder=”Enter the number to be incremented by”>
<button (click)=”incrementCounter()”>Increment Counter</button>

<button (click)=”decrementCounter()”>Decrement Counter</button>
<button (click)=”resetCounter()”>Reset Counter</button>

To understand how these methods and properties work, lets check the AppComponent Class.

Let’s begin.

currentCount$:Observable<number>|undefined;
incrementBy:number=1;

constructor(private store:Store<{[stateSliceKey:string]:number}>){
this.currentCount$=this.store.select(counterSelector);
}

=>As you can see above, we have first injected a reference store of the Store class(imported from @ngrx/store). We will use the Store class reference to dispatch actions and also select the desired state slice from the store.

=>We are connecting the “myCountState” slice of the application state to the currentCount$ observable.

Observe that we have passed {[stateSliceKey:string]:number} as type parameter to the Store class in the constructor. Recall that the stateSliceKey constant contains the string “myCountState”.

We are calling the select() on the store reference, passing the counterSelector Selector function as argument. The select() returns an observable, which we are assigning to the currentCount$ observable. This means that every time, we increment/decrement/reset the counter, we are manipulating the “myCountState” slice of the application state and this in turn reflects in the component template.

this.currentCount$=this.store.select(counterSelector);

The counterSelector function is defined and exported from the counter.selectors.ts file.

import { createFeatureSelector } from “@ngrx/store”;
import { stateSliceKey } from “./counter.reducer”;
export const counterSelector= createFeatureSelector<number>(stateSliceKey);

We have used createFeatureSelector() which is a convenience method for returning a top level feature state. We are using it to extract the “myCountState” slice from the application state.

If you recall, we are subscribing to currentCount$ observable via async pipe in the template.

<p>Count: {{currentCount$|async}}</p>

=>incrementBy is the ngModel for the <input> element used to fix the amount by which the counter should be incremented. The default value of incrementBy is 1.

<input [(ngModel)]=”incrementBy” type=”number” placeholder=”Enter the number to be incremented by”>

=>When we click on the “Increment Counter” ,”Decrement Counter” or “Reset Counter” buttons, we are calling the methods incrementCounter(), decrementCounter() and resetCounter() respectively.

incrementCounter(){
console.log("incrementing counter")
const actionToBeDispatched=incrementCounterAction({incrementBy:this.incrementBy});
console.log(actionToBeDispatched);
this.store.dispatch(actionToBeDispatched);
}

decrementCounter(){
console.log("decrementing counter")
const actionToBeDispatched= decrementCounterAction();
console.log(actionToBeDispatched);
this.store.dispatch(actionToBeDispatched)
}

resetCounter(){
console.log("resetting counter")
const actionToBeDispatched=resetCounterAction();
console.log(actionToBeDispatched);
this.store.dispatch(actionToBeDispatched);
}

With ngrx, the component will not directly update the “countState” slice of the application state. This job is delegated to the reducer as discussed earlier. But how do I inform the reducer that the “countState” state needs to be updated a certain way, when I click on any of these 3 buttons ?

This is where an Action is required. An Action represents a unique
event dispatched from components and/or services.

Thus in our example, when the user clicks on any of the 3 buttons, the component needs to dispatch an action or in other words, dispatch an event.

Before proceeding to check, how we are dispatching the action, lets first check, how the actions are defined.

3. Where are the actions defined ?

We have defined all the actions for this counter functionality in the counter.actions.ts file. The actions defined below describe the possible state transitions that will be handled by the reducer function later in the story.

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

export const incrementCounterAction=createAction('[AppComponent]Increment',props<{incrementBy:number}>());
export const decrementCounterAction=createAction('[AppComponent]Decrement');
export const resetCounterAction=createAction('[AppComponent]Reset');

The createAction() creates a Creator function, which when called, returns an object in the shape of the Action interface. Since there are aren’t actions dispatched from multiple sources, we haven’t used createActionGroup() which is preferred over createAction() in scenarios where there are many actions dispatched from different sources and you need to group them based on the source. In Part-II of this series, we have used createActionGroup().

interface Action {
type: string;
}

This means that calling the createAction(), creates a Creator function, which when in turn is called, returns an object that models the above Action interface. The object could also contain additional optional metadata. We will discuss that shortly.

The interface has a single property: type, represented as a string. The type property is for describing the action that will be dispatched in the application. The value of the type comes in the form of [Source] Event and is used to provide a context of what category of action it is, and where the action was dispatched from. This means that we must try to add as many actions are possible. Actions must be written based on the source of the event. For every source, a different set of actions.

With this information, lets list down our observations of each statement in the counter.actions.ts file.

export const incrementCounter=createAction(‘[AppComponent]Increment’,props<{incrementBy:number}>());

In the createAction() above, the first mandatory argument , ‘[AppComponent]Increment’ represents the value of the type field of the Action interface.

The 2nd optional argument contains any additional metadata. The props method is used to define the metadata needed for the handling of the action. Why do we need additional data to be passed in the incrementCounter() ? If you recall, we have given the user an option to decide, the amount by which the counter value should be incremented. Thus the constant incrementCounterAction, contains the Creator function. The function call will return an object with the “type” key and “incrementBy” key.

Note that the metadata passed to the action can be accessed in the Reducer function which will handle the action.

export const decrementCounter=createAction(‘[AppComponent]Decrement’);
export const resetCounter=createAction(‘[AppComponent]Reset’);

Moving to the remaining 2 statements above in the counter.actions.ts. These are similar to what we have discussed. The only difference is in the metadata. There is no additional metadata passed when decrementing/ resetting the counter. The constants decrementCounterAction and resetCounterAction will also contain Creator functions, which are called when the action is dispatched. The function call will return an object with only the “type” key. This is because there is no additional metadata. Hence no additional keys in the object returned.

4. How are the actions dispatched ?

Now that we know, how our actions are defined, lets jump back to the AppComponent class, to see how they are dispatched.

Lets begin with the incrementCounter() in the class, which is called when the user clicks on the “Increment Counter” button.

incrementCounter(){
console.log("incrementing counter")
const actionToBeDispatched=incrementCounterAction({incrementBy:this.incrementBy});
console.log(actionToBeDispatched);
this.store.dispatch(actionToBeDispatched);
}

We have first called the Creator function stored in the constant incrementCounterAction (exported from the counter.actions.ts).

const actionToBeDispatched=incrementCounterAction({incrementBy:this.incrementBy});

We have passed the additional metadata as argument to this Creator function. Calling the function returns an object as in the below screenshot. This object is stored in a constant actionToBeDispatched.

this.store.dispatch(actionToBeDispatched);

Finally, to dispatch the action, we have called the dispatch method on the store reference and passed the object stored in the constant actionToBeDispatched as argument to the dispatch method.

For the remaining 2 methods: decrementCounter() and resetCounter(), we are doing something similar.

decrementCounter(){
console.log("decrementing counter")
const actionToBeDispatched= decrementCounterAction();
console.log(actionToBeDispatched);
this.store.dispatch(actionToBeDispatched)
}

resetCounter(){
console.log("resetting counter")
const actionToBeDispatched=resetCounterAction();
console.log(actionToBeDispatched);
this.store.dispatch(actionToBeDispatched);
}

First call the Creator functions stored in the constants decrementCounterAction and resetCounterAction(exported from counter.actions.ts). Calling the functions, returns an object as in the below screenshot.

Finally, we have called the dispatch method on the store reference and passed the object as argument to the dispatch method.

Below is a short demonstration of what we have covered as of now.

5. How will the state transition take place ?

We have now successfully dispatched the action. We now need a reducer to act on the action and update the state. We have defined the reducer function for the counter functionality inside counter.reducer.ts.

import { createReducer, on } from "@ngrx/store";
import { decrementCounterAction, incrementCounterAction, resetCounterAction } from "./counter.actions";

let initialState:number=0;

export const stateSliceKey:string="myCountState";

export const counterReducer=createReducer(
initialState,
on(incrementCounterAction,(state,props)=>{
console.log(`Original value of state is ${state}`)
console.log(`Reducer Acting on ${props.type}. Incrementing counter by ${props.incrementBy}`)
const newState=state+props.incrementBy;
console.log(`New value of state is ${newState}`);
return newState;
}),
on(decrementCounterAction,(state,props)=>{
console.log(`Original value of state is ${state}`)
console.log(`Reducer Acting on ${props.type}`) ;
const newState=state-1;
console.log(`New value of state is ${newState}`);
return newState;
}),
on(resetCounterAction,(state,props)=>{
console.log(`Original value of state is ${state}`)
console.log(`Reducer Acting on ${props.type}`) ;
const newState=initialState;
console.log(`New value of state is ${newState}`);
return newState;
})
)

=>We have exported the below string to be used for mapping the state slice “myCountState” with the reducer function in the AppModule. We will see how this is done towards the end of the story.

export const stateSliceKey:string="myCountState";

=> The reducer function’s responsibility is to handle the state transitions in an immutable way. This means that the state transitions are not modifying the original state, but are returning a new state object using the spread operator. In case of primitive types, we do not require any spread operator. Note that spread operator will not work for deeply nested objects. Libraries like lodash and immer will do the job in those scenarios.

=>We have created a single reducer function: counterReducer using the createReducer(). The reducer function: counterReducer is nothing but a listener of actions. The function will handle the 3 actions: [AppComponent]Increment, [AppComponent]Decrement and [AppComponent]Reset.

Inside the createReducer(), the first argument sets the initial value of the state slice “myCountState”. The initialState property gives the state slice an initial value. Since the initial value of the counter is 0, we have set the initialState to 0.

let initialState:number=0;

The remaining arguments are on functions that are associating a single or multiple action with a state transition. When an action is dispatched, all the registered reducers receive the action. Whether they handle the action or not is determined by the on functions.

For instance, in the first on function below, the first argument is the action: incrementCounterAction(exported from counter.actions.ts) and the 2nd argument is a function, where we are able to access the value of the state before transition and also the metadata we passed to the action. Using the original state and the optional metadata(if any), we are computing the new state and returning it.

on(incrementCounterAction,(state,props)=>{

console.log(`Original value of state is ${state}`)
console.log(`Reducer Acting on ${props.type}. Incrementing counter by ${props.incrementBy}`)

const newState=state+props.incrementBy;
console.log(`New value of state is ${newState}`);
return newState;
}),

Below is a short demonstration of actions and reducer working together. Observe the browser console for the original value of state and the new state value.

6. Registering the Reducer function

Lets now update the AppModule, to map the “myCountState” slice with the reducer function: counterReducer.

app.module.ts

We have updated FROM

StoreModule.forRoot({})

TO

StoreModule.forRoot({[stateSliceKey]:counterReducer})

If you recall, we exported the constant stateSlicekey from counter.reducer.ts.

export const stateSliceKey:string=”myCountState”;

We are adding “myCountState” as the key of the object and the reducer function: counterReducer as the value. This means that we are registering the reducer function: counterReducer to manage a single slice of state “myCountState”.

If we do not register the reducer function with the AppModule or any feature module, the reducer function will not execute when an action is dispatched. Thus this is a very essential step for reducers to work.

Below is the link to the git repository for this example.

--

--

Angular&NodeEnthusiast
Angular&NodeEnthusiast

Written by Angular&NodeEnthusiast

Loves Angular and Node. I wish to target issues that front end developers struggle with the most.

No responses yet