Exploring Angular Lifecycle Hooks – OnChanges blog post

Exploring Angular Lifecycle Hooks – OnChanges

Wes Grimes

29 Oct, 2019

Angular

8 minutes read

Welcome back to our blog series, Exploring Angular Lifecycle Hooks!

Available Lifecycle Hooks covered in this series:

Let’s continue the series with one of the under-utilized, yet extremely helpful hooks, ngOnChanges.

According to the Angular Docs, OnChanges is used to “Respond when Angular (re)sets data-bound input properties. The method receives a SimpleChanges object of current and previous property values. Called before ngOnInit() and whenever one or more data-bound input properties change.”

In plain English, this lifecycle hook will allow us to monitor the value of Inputs to our components and directives and allow us to branch our logic to react differently when those values do change.

In this article, we will review how to implement OnChanges, a common use case for OnChanges, and a potential alternative using setters.

Angular ngOnChanges

OnChanges is an Angular lifecycle method, that can be hooked into components and directives in Angular. By defining a specific method named ngOnChanges on our class, we are letting the Angular runtime know that it should call our method at the appropriate time. This allows us to implement logic in our classes to handle updates to our changing Input data.

Implementing OnChanges

In order to implement OnChanges, we will follow two simple steps.

Add OnChanges after the implements keyword

The first step to implementing OnChanges is to add OnChanges after the implements keyword on a component or directive.

Here’s a common component that lacks lifecycle hooks:

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

@Component({...})
export class SomeCoolComponent {}

Let’s import OnChanges from Angular’s core package. Once imported we can create a contract with implements OnChanges:

import { Component, OnChanges } from '@angular/core';

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

Fun Fact Time: Technically it’s not required to implement the interface, Angular will call ngOnChanges regardless, however, it’s very helpful for type-checking, and to allow other developers to quickly identify which lifecycle hooks are in use on this class.

Add the ngOnChanges method to our class

With our newly added OnChanges after implements TypeScript IntelliSense will underline the class declaration in red, giving a warning that ngOnChanges was not found. We can resolve that issue by creating our ngOnChanges method.

Example Component Before:

import { Component, OnChanges } from '@angular/core';

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

Example Component After:

import { Component, OnChanges } from '@angular/core';

@Component({...})
export class SomeCoolComponent implements OnChanges {
    ngOnChanges(changes: SimpleChanges) {
        // Input change handling logic goes here   
    }
}

The SimpleChanges Object

As you can see above, the ngOnChanges method takes in a changes: SimpleChanges parameter. SimpleChanges is an object that will have a property for each Input defined in your component or directive.

Here is the shape of the SimpleChanges object:

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

Each property defined in SimpleChanges will have a child SimpleChange object:

interface SimpleChange {
    currentValue: any;
    previousValue: any;
    firstChange: boolean;
    isFirstChange(): boolean;
}
  • currentValue – This property will contain the value of the Input at the time this method was fired
  • firstChange – This property will contain a boolean value of whether or not this is the first time the value has changed. The first time a value comes through an Input counts as a “change” and therefore will reflect true here. Subsequent changes will be false. This can be helpful if your component or directive needs to behave differently based on when the value changed.
  • previousValue – This property will contain the last value of the Input before this change happened. This can be helpful when comparing current to previous values, especially if you need to display to the user a “before” and “after” state.
  • isFirstChange() – This is a helper method that returns true if this is the first time this value has changed.

As you can see, the SimpleChange object can be really helpful. It allows us to inspect the changes flowing through ngOnChanges and make intelligent decisions in our logic based on the values in this object.

OnChanges In The Real World

Implementing OnChanges was a simple two-step process. Let’s dive in and review a real-world use case for OnChanges. At the beginning of the article, we mentioned that Angular recommends the following: “Respond when Angular (re)sets data-bound input properties. The method receives a SimpleChanges object of current and previous property values. Called before ngOnInit() and whenever one or more data-bound input properties change.”

Revisiting The Github Repository Explorer Example

Let’s revisit an example from my previous OnInit article in this series, the Github Repository Explorer.

If we remember correctly, we had a component named GithubReposComponent, that had an Input for the repoLimit. In the example, we initialized our repos$ with a call to the GithubService.getMostStarredRepos and passed in the repoLimit.

Here’s the full component:

// github-repos.component.ts

import { Component, OnInit, Input } from '@angular/core';
import { Observable } from 'rxjs';

import { GithubService, GithubRepo } from './github.service';

@Component({
  template: `
    <app-github-repo 
      *ngFor="let repo of (repos$ | async)"
      [githubRepo]="repo">
    </app-github-repo>`
})
export class GithubReposComponent implements OnInit {
  @Input() repoLimit: number;

  repos$: Observable<GithubRepo[]>;

  constructor(private githubService: GithubService) {}

  ngOnInit() {
    this.repos$ = this.githubService.getMostStarredRepos(this.repoLimit);
  }
}

OnChanges, the hero we all need

If we are handling the repoLimit Input in the ngOnInit, we might say to ourselves: “what’s the problem?” Well, the problem is that we only handle repoLimit in the ngOnInit. This means that if we were to have a new value flow down from the parent in the repoLimit Input our repos$ would not re-retrieve the new set of repos with the new limit.

How do we fix our component so that our repos$ are re-retrieved every time repoLimit changes? Well, this is where our new hero, OnChanges comes to the rescue.

Let’s implement OnChanges and add our new ngOnChanges(changes: SimpleChanges) method to our component. Inside that new method, let’s check for changes.repoLimit to be truthy and if so, then let’s initialize our repos$ observable to a service call passing in the changes.repoLimit.currentValue to retrieve the latest value for the repoLimit Input.

// github-repos.component.ts

import { Component, OnChanges, Input } from '@angular/core';
import { Observable } from 'rxjs';

import { GithubService, GithubRepo } from './github.service';

@Component({
  template: `
    <app-github-repo 
      *ngFor="let repo of (repos$ | async)"
      [githubRepo]="repo">
    </app-github-repo>`
})
export class GithubReposComponent implements OnChanges {
  @Input() repoLimit: number;

  repos$: Observable<GithubRepo[]>;

  constructor(private githubService: GithubService) {}

  ngOnChanges(changes: SimpleChanges) {
    if (changes.repoLimit) {
      this.repos$ = this.githubService.getMostStarredRepos(changes.repoLimit.currentValue);
    }    
  }
}

Fantastic! Now our component will re-retrieve our repos$ every time repoLimit changes.

Setters vs ngOnChanges

Reviewing the previous example, let’s refactor our component a bit more and make use of an alternative to OnChanges that will also allow us to re-retrieve our repos$ each time repoLimit changes. To do so, we will convert the repoLimit Input into a TypeScript setter using the set syntax.

Creating a refreshRepos Method

First, let’s create a new method named refreshRepos(limit: number) and move the repos$ initialization into that new method. Our new refreshRepos method should look like this:

refreshRepos(limit: number) {
  this.repos$ = this.githubService.getMostStarredRepos(limit);
}

Removing the OnChanges Implementation

Next, let’s remove the OnChanges implementation from our component, first removing the implements OnChanges and then removing the ngOnChanges method altogether.

Our class declaration will look something like this with OnChanges and ngOnChanges removed:

export class GithubReposComponent {...}

Converting the repoLimit Input to a setter

TypeScript setters provide a way to define a method that is called every time the value on the class is set or changed.

Now, let’s add a setter to our Input() repoLimit: number. In the set for repoLimit we will make a call to our refreshRepos method passing in the newLimit.

Our repoLimit setter will look like this:

@Input() set repoLimit(newLimit: number) {
  this.refreshRepos(newLimit);
}

A Refactored Component

Congratulations! We have completed refactoring our component to use a setter instead of OnChanges. This provides a simpler solution to our problem.

The finished component will look like this:

// github-repos.component.ts

import { Component, Input } from '@angular/core';
import { Observable } from 'rxjs';

import { GithubService, GithubRepo } from './github.service';

@Component({
  template: `
    <app-github-repo 
      *ngFor="let repo of (repos$ | async)"
      [githubRepo]="repo">
    </app-github-repo>`
})
export class GithubReposComponent {
  @Input() set repoLimit(newLimit: number) {
    this.refreshRepos(newLimit);
  }

  repos$: Observable<GithubRepo[]>;

  constructor(private githubService: GithubService) {}

  refreshRepos(limit: number) {
    this.repos$ = this.githubService.getMostStarredRepos(limit);
  }
}

As we review the above example, we might ask ourselves, does this still work on initialization? Well, the answer is yes! This is because the repoLimit setter is called when the Input is first set, and then every time thereafter it is changed.

Conclusion

Well, folks, we have reached the end of another article in this series on Angular Lifecycle hooks! If you take anything away from this article, I hope it is that OnChanges is powerful, but should be used wisely. And maybe, just maybe, you should consider using TypeScript setters instead.

About the author

Wes is a lead software engineer specializing in enterprise web apps. In his spare time, he enjoys mentoring new developers, tracking the weather, and contributing to open source projects like NgRx. If you follow him on Twitter you’re going to find a lot of pictures of food, sunsets, and tidbits about Angular.

Love the post? Share it!

Lots of time and effort go into all our blogs, resources and demos,
we'd love it if you'd spare a moment to share them!

Explore our Angular courses

Get started today and join over 60,000 developers.