HomeTechnologyOverview of NgRx Benefits and Advantages

Overview of NgRx Benefits and Advantages

If a team lead instructs a developer to write a lot of boilerplate code instead of writing a few methods to solve a sure dispute, they need convincing arguments. Software engineers are dispute solvers; they prefer to automate things and avoid unnecessary boilerplate.

Even though NgRx comes with some boilerplate code, it also provides powerful tools for development. This article demonstrates that spending a bit more time writing code will yield benefits that make it worth the effort.

Most developers initiated using state management when Dan Abramov launched the Redux library. Some initiated using state management because it was a trend, not because they lacked it. Developers using a standard “Hello World” project for state management could quickly find themselves writing the same code over and over again, increasing complexity for no achieve.

Eventually, some became frustrated and abandoned state management entirely.

My Initial Problem With NgRx

I think this boilerplate concern was a major issue with NgRx. At first, we weren’t able to see the huge picture behind it. NgRx is a library, not a programming paradigm or a mindset. However, in order to fully grasp the functionality and usability of this library, we have to expand our knowledge a bit more and focus on practical programming. That’s when you might get to write boilerplate code and feel pleased about it. (I mean it.) I was once an NgRx skeptic; now I’m an NgRx admirer.

A while ago, I initiated using state management. I went through the boilerplate experience described above, so I decided to stop using the library. Since I love JavaScript, I try to attain at least a fundamental knowledge about all wellliked frameworks in use today. Here’s what I learned while using React.

React has a feature called Hooks. Much like Components in Angular, Hooks are simple functions that accept arguments and return values. A hook can have a state in it, which is called a side effect. So, for example, a simple button in Angular could be translated into React like this:

@Component({
  selector: 'simple-button',
  template: ` <button>Hello {{ name }}</button> `,
})
export class SimpleButtonComponent {
  @Input()
  name!: string;
}

 
export default function SimpleButton(props: { name: string }) {
  return <button>{props.name} </button>;
}

As you can see, this is a straightforward transformation:

  • SimpleButtonComponent => SimpleButton
  • @Input() name => props.name
  • template => return value

Illustration: Angular Component and React Hooks are quite similar.
Angular Component and React Hooks are quite identical.

Our React function SimpleButton has 1 necessary characteristic in the practical programming world: It is a pure function. If you are reading this, I presume that you have heard that term at least once. NgRx.io cites pure functions twice in the Key Concepts:

  • 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.

In React, developers are inspired to use Hooks as pures functions as much as possible. Angular also encourages developers to implement the same pattern using the Smart-Dumb Component paradigm.

That’s when I realized that I lacked some crucial practical programming skills. It did not take lengthy to grasp NgRx, as after learning the key concepts of practical programming, I had an “Aha! moment”: I had improved my understanding of NgRx and needed to use it more to better understand the benefits it offers.

This article shares my learning experience and the knowledge I gained about NgRx and practical programming. I do not elaborate the API for NgRx or how to call actions or use selectors. Instead, I share why I’ve approach to respect that NgRx is a worthy library: It’s not just a relatively new trend, it provides a host of benefits.

Let’s start with practical programming.

Functional Programming

Functional programming is a paradigm that differs greatly from other paradigms. This is a very complex topic with many definitions and guidelines. However, practical programming contains some core concepts and knowing them is a prerequisite for mastering NgRx (and JavaScript in general).

These core concepts are:

  • Pure Function
  • Immutable State
  • Side Effect

I repeat: It’s just a paradigm, nothing more. There is no library practical.js that we download and use to write practical software. It’s just a way of considering about writing applications. Let’s start with the most necessary core concept: pure function.

Pure Function

A function is considered a pure function if it follows 2 simple rules:

  • Passing the same arguments always returns the same value
  • Lacking an observable side effect involved inside function execution (an external state change, calling I/O operation, etc.)

So a pure function is just a clear function that accepts some arguments (or no arguments at all) and returns an anticipated value. You are assured that calling this function will not result in side effects, like networking or changing some global user state.

Let’s take a gaze at 3 simple examples:

//Pure function
function add(a,b){
	return a + b;
}

//Impure function breaking rule 1
function random(){
	return Math.random();
}

//Impure function breaking rule 2
function sayHello(name){
	console.log("Hello " + name);
}
  • The first function is pure because it will always return the same answer when passing the same arguments.
  • The second function is not pure because it is nondeterministic and returns different answers every time it is called.
  • The third function is not pure because it uses a side effect (calling console.log).

It is easy to discern if the function is pure or not. Why is a pure function better than impure 1? Because it is simpler to think about. Imagine you are reading some source code and see a function call that you know is pure. If the function name is right, you don’t need to discover it; you know that it doesn’t change anything, it returns what you expect. It’s crucial for debugging when you have a huge enterprise application with a lot of business logic, as it can be a huge timesaver.

Also, it’s simple to test. You don’t have to inject anything or mock some functions inside it, you just pass arguments and test if the result is a match. There is a strong connection between the test and logic: If a component is easy to test, it is easy to understand how and why it works.

Pure functions approach with a very handy and performance-friendly functionality called memoization. If we know that calling the same arguments will return the same value then we can simply cache the results and not waste time calling it again. NgRx definitely sits on top of memoization; that’s 1 of the main causes why it is quickly.

The transformation should be intuitive and clear.

You may ask yourself, “What about side effects? Where do they go?” In his GOTO talk, Russ Olsen jokes that our clients don’t pay us for pure functions, they pay us for side effects. That’s true: Nobody cares about the Calculator pure function if it doesn’t get printed someplace. Side effects have their place in the practical programming universe. We will see that shortly.

For now, let’s move to the next step in maintaining complex application architectures, the next core concept: immutable state.

Immutable State

There is a simple definition for an immutable state:

  • You can only create or delete a state. You can’t update it.

In simple terms, to update a User object’s age … :

let user = { username:"admin", age:28 }

… you should write it like this:

// Not like this
newUser.age = 30;

// But like this
let newUser = {...user, age:29 }

Every change is a new object that has copied properties from the old ones. As such, we are already in a form of immutable state.

String, Boolean, and Number all are immutable states: You can’t append or modify existing values. In contrast, a Date is a mutable object: You always manipulate the same date object.

Immutability applies across the application: If you pass a user object inside the function that changes its age, it should not change a user object, it should create a new user object with an updated age and return it:

function updateAge(user, age) {
	return {...user, age: age)
}

let user = {username: 'admin', age: 29};

let newUser = updateAge(user, 32);

Why should we dedicate time and consideration to this? There are a couple of benefits worth underscoring.

One advantage for back-end programming languages includes parallel processing. If a state change doesn’t depend on a reference and every update is a new object, you can split the process into chunks and work on the same task with countless threads without sharing the same memory. You can even parallelize tasks across servers.

For frameworks such as Angular and React, parallel processing is 1 of the more beneficial ways of improving application performance. For example, Angular has to check every object’s properties that you pass via Input bindings to discern if a component has to be rerendered or not. But if we set ChangeDetectionStrategy.OnPush instead of the default, it will check by reference and not by each property. In a large application, this definitely saves time. If we update our state immutably, we get this performance boost for free.

The other advantage for an immutable state that all programming languages and frameworks share is identical to the benefits of pure functions: It’s simpler to think about and test. When a change is a new state born from an old 1, you know exactly what you are working on and you can track exactly how and where the state changed. You don’t lose the update history and you can undo/redo changes for the state (React DevTools is 1 example).

However, if a single state gets updated, you won’t know the history of those changes. Think of an immutable state like the transaction history for a bank account. It’s practically a should-have.

Now that we’ve reviewed immutability and pureness, let’s tackle the remaining core concept: side effect.

Side Effect

We can generalize the definition of a side effect:

  • In computer science, an operation, function, or expression is said to have a side effect if it modifies some state variable value(s) outdoors its native environment. That is to say it has an observable effect besides returning a value (the main effect) to the invoker of the operation.

Simply put, everything that changes a state outdoors the function scope—all the I/O operations and some work that is not immediately related to the function—can be considered a side effect. However, we have to avoid using side effects inside pure functions because side effects contradict practical programming philosophy. If you use an I/O operation inside a pure function, then it ceases to be a pure function.

Nevertheless, we need to have side effects someplace, as an application without them would be pointless. In Angular, not only do pure functions need to be protected against side effects, we have to avoid using them in Components and Directives as well.

Let’s examine how we can implement the beauty of this technique inside the Angular framework.

Illustration: NgRx Angular Side Effect

Functional Angular Programming

One of the first things to understand about Angular is the need to decouple components into smaller components as often as possible to enable simpler maintenance and testing. This is necessary, as we need to divide our business logic. Also, Angular developers are inspired to leave components only for rendering purposes and move all the business logic inside the services.

To expand on these concepts, Angular users added the “Dumb-Smart Component” pattern to their vocabulary. This pattern requires that service calls do not exist inside the small components. Because business logic resides in services, we nonetheless have to call these service methods, wait for their response, and only then make any state changes. So, components have some behavioral logic inside them.

To avoid that, we can create 1 Smart Component (Root Component), which contains business and behavior logic, pass the states via Input Properties, and call actions listening to Output parameters. This way, the small components are truly only for rendering purposes. Of course, our Root Component has to have some service calls inside it and we can’t just remove them but its utility would be restricted to business logic only, not rendering.

Let’s gaze at a Counter Component example. A counter is a component that has 2 buttons that increase or decrease value, and 1 displayField that displays the currentValue. So we end up with 4 components:

  • CounterContainer
  • IncreaseButton
  • DecreaseButton
  • CurrentValue

All the logic lives inside the CounterContainer, so all 3 are just renderers. Here’s the code for the 3 of them:

@Component({
  selector: 'decrease-button',
  template: `<button (click)="increase.emit()" [disabled]="disabled">
    Decrease
  </button>`,
})
export class DecreaseButtonComponent {
  @Input()
  disabled!: boolean;

  @Output()
  increase = new EventEmitter();
}

@Component({
  selector: 'current-value',
  template: `<button>
    {{ currentValue }}
  </button>`,
})
export class CurrentValueComponent {
  @Input()
  currentValue!: string;
}

@Component({
  selector: 'increase-button',
  template: `<button (click)="increase.emit()" [disabled]="disabled">
    Increase
  </button>`,
})
export class IncreaseButtonComponent {
  @Input()
  disabled!: boolean;

  @Output()
  increase = new EventEmitter();
}

Look how simple and pure they are. They have no state or side effects, they just depend on enter properties and emitting events. Imagine how easy it is to test them. We can call them pure components as that’s what they truly are. They depend only on enter parameters, have no side effects, and always return the same value (template string) by passing the same parameters.

So pure functions in practical programming are transferred into the pure components in Angular. But where does all the logic go? The logic is nonetheless there but in a slightly different place, namely the CounterComponent.

@Component({
  selector: 'counter-container',
  template: `
    <decrease-button [disabled]="decreaseIsDisabled" (decrease)="decrease()">
    </decrease-button>
    <current-value [currentValue]="currentValue"> </current-value>
    <increase-button (increase)="increase()" [disabled]="increaseIsDisabled">
    </increase-button>
  `,
})
export class CounterContainerComponent implements OnInit {
  @Input()
  disabled!: boolean;

  currentValue = 0;

  get decreaseIsDisabled() {
    return this.currentValue === 0;
  }

  get increaseIsDisabled() {
    return this.currentValue === 100;
  }

  constructor() {}

  ngOnInit(): void {}

  decrease() {
    this.currentValue -= 1;
  }

  increase() {
    this.currentValue += 1;
  }
}

As you can see, the behavior logic lives in CounterContainer but the rendering part is missing (it declares components inside the template) because the rendering part is for pure components.

We could inject as much service as we want because we handle all data manipulations and state changes here. One thing worth mentioning is that if we have a deep nested component, we should not create only 1 root-level component. We can divide it into smaller smart components and use the same pattern. Ultimately, it depends on the complexity and nested level for each component.

We can easily leap from that pattern into the NgRx library itself, which is just 1 layer above it.

NgRx Library

We can divide any web application into 3 core parts:

  • Business Logic
  • Application State
  • Rendering Logic

Illustration of business logic, application state, and rendering logic.

Business Logic is all the behavior that is happening to the application, such as networking, enter, output, API, etc.

Application State is the state of the application. It can be global, as the currently authorized user, and also native, as the current Counter Component value.

Rendering Logic encompasses rendering, like displaying data using DOM, creating or removing elements, and so on.

By using the Dumb-Smart pattern we decoupled Rendering Logic from Business Logic and Application State but we can divide them too because they are both conceptually different from each other. Application State is like a snapshot of your app in current time. Business Logic is like a static functionality that’s always present in your app. The most necessary reason to divide them is that Business Logic is principally a side effect that we want to avoid in application code as much as possible. This is when the NgRx library, with its practical paradigm, shines.

With NgRx you decouple all these parts. There are 3 main parts:

  • Reducers
  • Actions
  • Selectors

Combined with practical programming, all 3 combine to give us a powerful tool to handle applications of any size. Let’s examine each of them.

Reducers

A reducer is a pure function, which has a simple signature. It takes an old state as a parameter and returns a new state, either derived from the old 1 or a new 1. The state itself is a single object, which lives with your application’s lifecycle. It’s like an HTML tag, a single root object.

You cannot immediately modify a state object, you need to modify it with reducers. That has a number of benefits:

  • The change state logic lives in a single place, and you know where and how the state changes.
  • The reducer functions are pure functions, which are easy to test and handle.
  • Because reducers are pure functions, they can be memoized, making it possible to cache them and avoid extra computation.
  • State changes are immutable. You never update the same instance. Instead, you always return a new 1. This enables a “time travel” debugging experience.

This is a trivial example of a reducer:

function usernameReducer(oldState, username) {
	return {...oldState, username}
}

Even though it’s a very simple dummy reducer, it is the skeleton for all lengthy and complex reducers. They all share the same benefits. We could have hundreds of reducers in our application and we can make as many as we want.

For our Counter Component, our state and reducers could gaze like this:

interface State{
	decreaseDisabled:boolean;
	increaseDisabled:boolean;
	currentValue:number;
}

const MIN_VALUE=0;
const MAX_VALUE =100;

function decreaseReducer(oldState) {
	const newValue = oldState.currentValue -1
	return {...oldState,currentValue : newValue, decreaseDisabled: newValue===MIN_VALUE
}

function increaseReducer(oldState) {
	const newValue = oldState.currentValue + 1
	return {...oldState,currentValue : newValue, decreaseDisabled: newValue===MAX_VALUE
}

We removed the state from the component. Now we need a way to update our state and call the appropriate reducer. That’s when actions approach into play.

Actions

An action is a way to notify NgRx to call a reducer and update the state. Without that, it would be meaningless to use NgRx. An action is a simple object that we attach to the current reducer. After calling it, the appropriate reducer will be called, so in our example the we could have the following actions:

enum CounterActions {
  IncreaseValue="[Counter Component] Increase Value",
  DecreaseValue="[Counter Component] Decrease Value",
}

on(CounterActions.IncreaseValue,increaseReducer);
on(CounterActions.DecreaseValue,decreaseReducer);

Our actions are attached to the reducers. Now we can modify our Container Component further and call appropriate actions when necessary:

@Component({
  selector: 'counter-container',
  template: `
    <decrease-button [disabled]="decreaseIsDisabled" (decrease)="decrease()">
    </decrease-button>
    <current-value [currentValue]="currentValue"> </current-value>
    <increase-button (increase)="increase()" [disabled]="increaseIsDisabled">
    </increase-button>
  `,
})
export class CounterContainerComponent implements OnInit {
  constructor(private store: Store<any>) {}

  decrease() {
    this.store.dispatch(CounterActions.DicreaseValue);
  }

  increase() {
    this.store.dispatch(CounterActions.IncreaseValue);
  }
}

Note: We removed the state and we will add back shortly.

Now our CounterContainer doesn’t have any state change logic. It just knows what to dispatch. Now we need some way of displaying this data to the view. That’s the utility of selectors.

Selectors

A selector is also a very simple pure function, but unlike a reducer, it doesn’t update the state. As the name implies, the selector merely selects it. In our example, we could have 3 simple selectors:

function selectCurrentValue(state) {
	return state.currentValue;
}

function selectDicreaseIsDisabled(state) {
	return state.decreaseDisabled;
}

function selectIncreaseIsDisabled(state) {
	return state.increaseDisabled;
}

Using these selectors, we could select each slice of a state inside our smart CounterContainer component.

@Component({
  selector: 'counter-container',
  template: `
    <decrease-button
      [disabled]="ecreaseIsDisabled$ | async"
      (decrease)="decrease()"
    >
    </decrease-button>
    <current-value [currentValue]="currentValue$ | async"> </current-value>
    <increase-button
      (increase)="increase()"
      [disabled]="increaseIsDisabled$ | async"
    >
    </increase-button>
  `,
})
export class CounterContainerComponent implements OnInit {
  decreaseIsDisabled$ = this.store.select(selectDicreaseIsDisabled);
  increaseIsDisabled$ = this.store.select(selectIncreaseIsDisabled);
  currentValue$ = this.store.select(selectCurrentValue);

  constructor(private store: Store<any>) {}

  decrease() {
    this.store.dispatch(CounterActions.DicreaseValue);
  }

  increase() {
    this.store.dispatch(CounterActions.IncreaseValue);
  }
}

These selections are asynchronous by default (as are Observables in general). This has no significance, at least from a pattern point of view. The same would be true for a synchronous 1, as we would just select something from our state.

Let’s step back and gaze at the huge picture to see what we’ve achieved thus far. We have a Counter Application, which has 3 main parts that are almost decoupled from each other. Nobody knows how the application state is managing itself or how the rendering layer renders the state.

The decoupled parts use the bridge (Actions, Selectors) to connect to each other. They are decoupled to such an extent that we could take the whole State Application code and move it to another project, like for a mobile version for example. The only thing we would have to implement would be the rendering. But what about testing?

In my humble opinion, testing is the best part of NgRx. Testing this sample project is akin to playing tic-tac-toe. There are only pure functions and pure components, so testing them is a breeze. Now imagine if this project becomes larger, with hundreds of components. If we follow the same pattern, we would just add more and more pieces together. It would not become a messy, unreadable blob of source code.

We’re almost done. There’s only 1 necessary thing left to cover: side effects. I mentioned side effects many times so far but I stopped quick of explaining where to store them.

That’s because side effects are the icing on the cake and by building this pattern, it’s very easy to remove them from the application code.

Side Effects

Let’s say our Counter Application has a timer in it and every 3 seconds it routinely increases the value by 1. This is a simple side effect, which has to live someplace. It’s the same side effect, by definition, as an Ajax request.

If we think about side effects, most have 2 main causes to exist:

  • Doing anything outdoors the state environment
  • Updating application state

For example, storing some state inside the LocalStorage is the first option, while updating the state from the Ajax response is the second. But they both share the same signature: Each side effect has to have some starting point. It needs to be called at least once to prompt it to start the action.

As we outlined earlier, NgRx has a nice tool for giving somebody a command. That’s an action. We could call any side effect by dispatching an action. The pseudo code could gaze like this:

function startTimer(){
    setInterval(()=>{
 	console.log("Hello application");
    },3000)
}

on(CounterActions.StartTime,startTimer)
...
// We start timer by dispatching an action

dispatch(CounterActions.StartTime);

It’s pretty trivial. As I mentioned earlier, side effects either update something or not. If a side effect doesn’t update anything, there’s nothing to do; we just leave it. But if we want to update a state, how do we do it? The same way a component tries to update a state: calling another action. So we call an action inside the side effect, which updates the state:

function startTimer(store) {
    setInterval(()=> {
          // We are dispatching another action
 	    dispatch(CounterActions.IncreaseValue)
    }, 3000)
}

on(CounterActions.StartTime, startTimer);
...
// We start timer by dispatching an action

dispatch(CounterActions.StartTime);

We now have a fully practical application.

Summarizing Our NgRx Experience

There are some necessary topics I would like to mention before we finish our NgRx journey:

  • The code proven is simple pseudo code I invented for the article; it is only fit for demonstration purposes. NgRx is the place where real sources live.
  • There is no official guideline that proves my theory about connecting practical programming with the NgRx library. It’s just my opinion formed after reading dozens of articles and source code samples created by highly skilled people.
  • After using NgRx you will definitely realize that it’s way more complex than this simple example. My goal was not to make it gaze simpler than it actually is but to show you that even though it’s a bit complex and may even result in a longer path to the destination, it’s worth the added effort.
  • The worst utilization for NgRx is to use it everywhere, regardless of the size or complexity of the application. There are some cases in which you should not use NgRx; for example, in forms. It’s nearly impossible to implement forms inside NgRx. Forms are glued to the DOM itself; they can’t live individually. If you try to decouple them, you will find yourself hating not only NgRx but web technology in general.
  • Sometimes using the same boilerplate code, even for a small example, can turn into a nightmare, even if it can advantage us in the prospective. If that’s the case, just combine with another incredible library, which is a part of the NgRx ecosystem (ComponentStore).

Go to the source

Most Popular