How to make Angular OnChanges better

Picture of city.
Photo by Denys Nevozhai on Unsplash

Like most of you, I work in a team of a couple of programmers and our responsibility is to handle a few big Angular projects. In order to keep our project's codebase clean, readable and without any bugs we try to use verified Angular patterns. One of them is obviously the Smart and dumb components pattern which makes us create two types of components. The "dumb" one or also called the "presenter" should be simple without any dependencies, its only responsibility is to present data. As the pattern states it cannot inject any services, so all the data flow must come from the inputs. I find myself writing many of these simple components and despite frequent experiences with them, I found out they are the source of my headaches. In order to better understand my boomer problem let's take a look at the components code:

@Component({ ... })
export class UserComponent implements OnChanges {

   @Input()
   user: User;

   constructor(private readonly service: UserService) {}

   ngOnChanges(changes: SimpleChanges) {
      if (changes.user !== undefined &&
          changes.user.currentValue !== undefined) {

         service.setUser(changes.user)
      }
   }
}

Every one of the presenter components looks something like this. The problem is inside the ngOnChanges method. I want to run specific code only when one of the inputs changes, so I need to wrap everything around the if statement. This solution allows me to be precise, but it comes with a cost. As you can see the "if" statement is not straightforward and I find myself a lot asking "What is a proper if statement inside the ngOnChanges method?" In this article, I would like to show you what was the solution I came up with.

The theory

I don't want to bore you with the exact explanation of the inner mechanisms of the Angular components communication, so let's do it briefly. Basically, there are two most popular methods of component communication, one is through "Services" and the other through "Inputs". In this article, we will focus on the latter one.

The "Inputs" architecture is really straightforward. In the component class, a programmer declares a property decorated with the @Input decorator and then it is possible to pass values to the component through an HTML template.

@Component({
   selector: 'basic'
})
export class BasicComponent implements OnChanges {

   @Input()
   name: string;

   ngOnChanges(simpleChanges: SimpleChanges) {
      console.log(simpleChanges);
   }
}
// usage in a html file
<basic [name]="'Luke'"></basic>

What if we want to do something or modify the data when the new value appears on this component's input? Then we can use one of the components lifecycle hook OnChanges.

Lifecycle hooks implement the dependency inversion principle and they are Angulars answer to the problem of running code when something specific happens during the component life.

If you want to read more about various techniques regarding Angular input mechanism please check out this article "A deep dive into Angular Inputs".

The OnChanges hook

Let's have a deeper look at this hook. It takes a changes argument which is an object of type SimpleChanges:

interface OnChanges {
    ngOnChanges(changes: SimpleChanges): void;
}

The SimpleChanges is just an object with properties mapped to each component input. The more inputs the more properties it can potentially have. For this given component:

@Component({ ... })
export class BasicComponent implements OnChanges {

   @Input()
   name: string;

   @Input()
   description: string

   ngOnChanges(simpleChanges: SimpleChanges) {
      console.log(simpleChanges);
   }
}

Used like this:

<component [name]="'Great'" [description]="'Article!'" ></component>

Simple changes will look like this:

SimpleChanges object when two inputs change.
SimpleChanges object when two inputs change.

So the simple changes will have two properties name and description. If only one input changes its value e.g. name, the object simpleChanges will consist only of on property name.

SimpleChanges from change of one of the inputs.
SimpleChanges from change of one of the inputs.

Properties from the SimpleChanges object are represented as objects of type SimpleChange.

export declare interface SimpleChanges {
    [propName: string]: SimpleChange;
}

The interface SimpleChange has a couple of properties that allows to deal with more complex scenarios. From my experience, the most used one is the property currentValue.

export declare class SimpleChange {
    previousValue: any;
    currentValue: any;
    firstChange: boolean;
    constructor(previousValue: any, currentValue: any, firstChange: boolean);
    /**
     * Check whether the new value is the first value assigned.
     */
    isFirstChange(): boolean;
}

That's the theory now let's have a look at how it works in practice.

OnChanges in practice

I've noticed that many programmers use the same technique to check whether the input has changed or not:

ngOnChanges(changes: SimpleChanges) {
   if (changes.user !== undefined &&
       changes.user.currentValue !== undefined) {

         service.set(changes.user)
   }
}

First, we check whether the simpleChanges object has a property named exactly the same as the input we want to verify and then we check if the currentValue property is not undefined. This statement assures us that the input that we are interested in has changed.

The downside of this is that every time I want to check whether something has changed I need to use this complex "if" statement in order to be sure that this exact input has changed. I found out that I struggle with remembering the exact line of code and it happens every time I want to use this code. So what I do is I find a similar component and copy the if statement from it. The interface of the OnChanges lifecycle hook could be more developer friendly.

A software developer should not be forced to remember the code.
A software developer should not be forced to remember the code.

I came to a realization that my memory isn't perfect and I need to find a better solution for the future. So I created these simple utility functions:

function ifChanged(
   prop: SimpleChange,
   callback: (value: any) => void
): void {
   if (prop !== undefined && prop.currentValue !== undefined) {
      callback(prop.currentValue);
   }
}

Let's use them inside the component:

@Component({ ... })
export class UserComponent implements OnChanges {

   @Input()
   user: User;

   constructor(private readonly service: UserService) {}

   ngOnChanges(changes: SimpleChanges) {
      ifChanged(changes.user, () => service.set(changes.user))
   }
}

The ifChanged function makes the inside of the ngOnChanges method more readable. In my opinion, it is more semantically correct and what is the most interesting it allows you to forget the inner workings of the SimpleChanges object.

You can check the running example in this stackblitz project.

If the code is cleaner and simpler it gives you fewer possibilities of making a mistake. It is simpler to extend and maintain. In our team, we have been using this technique for a while and it made our code better. We've also noticed fewer questions on the team's slack channel "So what is the proper check in the OnChanges method".

Summary

The Angular framework has a great mechanism of components communication. The only downside of it is that it could be more developer friendly. I hope that the techniques I have presented in this article help Angular developers deal with issues that come from working with the OnChanges lifecycle hook. The utility functions can be used right away in every Angular project and can give instant value. I think that they speed up the development process, increase readability and reduce the number of bugs that may come up from unclear usage of the OnChanges hook. Please be my guest and use them, so you can have a better Angular experience.

Luke, Software engineer, passionate about best practices, frameworks React, Vue, patterns and architectures for modern web
				development, open-source creator, TypeScript enthusiast, Generic UI Technical Blog

Luke

Software engineer, passionate about best practices, frameworks React & Vue, patterns and architectures for modern web development, open-source creator, TypeScript enthusiast.

We use cookies to improve your experience. If you continue browsing, we assume that you consent to our use of cookies.