top of page
Writer's pictureThe Tech Platform

Angular State Management with NgRx

This guide aims on understanding why you may need local or global state management as well as get you started with NgRx which is a library that brings Redux-like reactive state management to Angular.



Getting Started


1 .Why use NgRx

One of the biggest challenges with any application is managing data. In angular there are many patterns we can follow to manage your application data, and most of the time these will involve using the Input or Output decorators to share objects between components (which i know can be painfull if your application handles a lot of data) or things like RxJs Observables to listen to data changes. Now NgRx solves this problem in a reactive, elegant way and also gives you amazing debugging features.


A quick guide line that might help you decide If you do need NgRx Store is the SHARI principle:

  • Shared: state that is accessed by many components and services.

  • Hydrated: state that is persisted and rehydrated from external storage.

  • Available: state that needs to be available when re-entering routes.

  • Retrieved: state that must be retrieved with a side-effect.

  • Impacted: state that is impacted by actions from other sources.


2. How it works

Letā€™s look at the high level principles of NgRx: Letā€™s say we have a user interface that is displaying some data, then the user performs some action that shall change the state of that data, in redux the only way to change the state of the data is by dispatching an action, once an action has been dispatched it goes through a reducer function which will copy the current state of the object along with any data changes into a new object, because the state is immutable meaning it canā€™t be changed directly it has to be copied over to a new state, after the reducer has created the new state it gets saved in a data store which can be thought as a client side database, then in NgRx we treat this store as an observable where we can subscribe to it from anywhere in our application this means that all if our components and services will be sharing the same data at any given point and time, the reason for this is it gives us a predictable tree of state changes which will later come in handy when we want to debug or test our application.


Key NgRx Concepts
  • Actions describe unique events that are dispatched from components and services.

  • State changes are handled by pure functions called reducers that take the current state and the latest action to compute a new state.

  • Selectors are pure functions used to select, derive and compose pieces of state.

  • State is accessed with the Store, an observable of state and an observer of actions.


Flow of application state in NgRx


3. Getting started

In our angular project we need to install the NgRx store lib using your preferred package manager Iā€™ll use npm in this case:

npm install @ngrx/store ā€” save

Weā€™re going to start with a pretty simple example where our entire application state is represented by this object which holds information about a product in our app. Then weā€™ll create two different actions to either add or remove the Product from ngrx/store.


Letā€™s first create a dedicated model for our objects:

exportin terface GenericObject<T>
{
    incomingObject: T
}

Since our app has multiples data objects (products, costumers etc) we create this interface with a generic type inferred so that we can add any object to the ngrx/store.


Then create a file called object.actions.ts


Here weā€™ll export constants which represent each action (in all caps by convention)


For each class representing the action well implement the Action interface, then create a read-only type that corresponds the action constant that we defined in all caps. To send an object with this action we have to define a constructor which has the payload property in this case is of type object, but could be any other type depending on the purpose.

import{Action}from '@ngrx/store'

export const DEFINE_OBJECT = '[OBJECT] Define';
export const RESET_OBJECT='[OBJECT] Reset';

export class DefineObject implements Action{
    readonly type = DEFINE_OBJECT;
    
    constructor(public payload: object)
    {
    }
}
    
export class ResetObject implements Action{
    readonly type = RESET_OBJECT;
}

The reset action has no constructor since weā€™re not trying to send any data with this action, only reset the state to the default app state. Then we export all this actions as a single type so they can be used for strong typing in other files


Letā€™s then build our reducer function.


The purpose of this function is to take the current state and pass it to the new state based on the changes the action is trying to make,


The default state is will be the initial state of our application, then whenever we want to create a new state we pass the new-State function which uses the Object.assign to pass first an empty object, then the state and then any new data, this is because the state object is immutable and we cannot just add properties to the object itself.


Now in the reducer function we pass in the state as well as the function, and using the switch statement execute the action based on the action type we send to the reducer function.

import * as ObjectActions from '../actions/object.actions';
import {GenericObject} from '../../../models/GenericObject';
import {Action} from '@ngrx/store'

export type actions = ObjectActions.Actions;

//default object state
const defaultState: GenericObject<any>={
    incomingObject: {'initState': 'initState'}
}

// create new state object
const newState=(state:any, newData:any)=>{
    return Object.assign({},state, newData)
}

//reducer function
export function ObjectReducer(state: GenericObject<any> = defaultState, incomingAction: Action)
{
    console.log(incomingAction.type,state)
    const action=incomingAction as actions;
    
    switch(action.type)
    {
    caseObjectActions.DEFINE_OBJECT:
        return newState(state,{incomingObject: action.payload})
    
    caseObjectActions.RESET_OBJECT:
       return defaultState;
    
    default:
        return state;
    }
}

Then in our app component:


We import the StoreModule form ngrx/store and the reducer function, we also import the StoreDevtoolsModule which we will see after why, after that on the imports metadata letā€™s add the ObjectReducer function.

//other imports
import {StoreModule} from '@ngrx/store';
import {ObjectReducer} from './state/reducers/object.reducer';
import {AppComponent} from './app.component';
import {StoreDevtoolsModule} from '@ngrx/store-devtools';

@NgModule(
{
    imports:      [
        //other imports
        StoreModule.forRoot({
            incomingObject$: ObjectReducer
        }),
        //let redux devtools store the last 10 states
        StoreDevtoolsModule.instrument({
            maxAge: 10
        })
    ],
    declarations: [
        AppComponent,
    ],
    bootstrap:    
        [AppComponent],
    providers:[
    {
        //your providers},
    ]
)

Now for our component:


Here we handle the button actions from our html component to trigger/dispatch the actions we need in this case add/remove the object to the ngrx/store and access the object at any component or service of our application

import {Location} from '@angular/common';
import {Component} from '@angular/core';
import {GenericObject} from '../models/GenericObject';
import {Observable} from 'rxjs';
import {Store} from '@ngrx/store';
import * as ObjectActions from './state/actions/object.actions'

declare var $:any;

export interface AppState{
    incomingObject$: GenericObject<any>
}

//base class to handle the button events and dispatch the actions
@Component ({selector: 'app-generic-component',})
export abstract class GenericComponent implements DataTable
{
    incomingObject$: Observable<GenericObject<any>>
    
    protected constructor(protected store?: Store<AppState>)
    {
        this.incomingObject$=this.store.select('incomingObject$')
    }
    
    rendersTable()
    {
        let _this=this;
        $(document).ready(function()
        {    
            var tableS=$('table.singleSelect').DataTable({
                "pagingType": "full_numbers",
                "lengthMenu": [
                    [10,25,50,-1],
                    [10,25,50,"All"]
                ],
                responsive: true,
                language: {
                    search: "_INPUT_",
                    searchPlaceholder: "Search",
                }
        });
        
        tableS.on('click', 'tr', function()
        {
            if($(this).hasClass('selected'))
            {
                $(this).removeClass('selected');
            
            }else{
               tableS.$('tr.selected').removeClass('selected');
               $(this).addClass('selected');
            }
            
            $('.btn-add').click(()=>
            {
                _this.store.dispatch(new TableActions.DefineObject
                    (JSON.parse(tableS.row('.selected').data())))
            });
            
            $('.btn-remove').click(()=>
            {
                _this.store.dispatch(new TableActions.ResetObject
                    (JSON.parse(tableS.row('.selected').data())))

Then inside any component or service we have access to the state object by subscribing to the observable incomingObject we defined in our GenericComponent.ts file

import {Component, OnInit} from '@angular/core';
import {AppState, GenericComponent}from '../../../../../shared/implementations/GenericComponent';
import {Location} from '@angular/common';
import {Store} from '@ngrx/store';
import {GenericObject} from '../../../../../shared/models/GenericObject';
import {Observable} from 'rxjs';

@Component({
    selector: 'my-component-list',
    templateUrl: './my.component.html',
    styleUrls: ['./my-component.component.css']
})

export class MyComponent extends GenericComponent implements OnInit{
    myObject: GenericObject;
    constructor(location: Location,
        store: Store<AppState>){
            super(location,store)
    }
    ngOnInit(): void{
        this.incomingObject$.subscribe(value=>
        {
            this.myObject=value.incomingObject;
        }

Then in any component or service we just subscribe to the observable and the can retrieve the state object

Now whenever we click the add or remove button our state changes. We can console.log() our app to see whatā€™s happening.



An even better way to see the state changes in our app is to use a chrome plugin called Redux DevTools, you enable it by installing the npm package: npm install @ngrx/store-devtools ā€” save and then installing the chrome plugin from the chrome store, then import it to the app.module.ts and you can set the number of app states it can store at a time (refer to the app.module.ts file above), if youā€™ve never used it youā€™ll fall in love with it since it gives you debugging superpowers.



You can even see a chart of how your state is at a given moment


Conclusion

In this tutorial, we have engaged in a quick way to build a manage the state of our application with NgRx, thereā€™s a lot more to NgRx (NgRx effects, Entities etc.,) which i will be covering in future posts.



Source: Medium - Ivan Lifanica


The Tech Platform

0 comments

Comments


bottom of page