Write Angular like a pro. Angular Icon

Follow the ultimate Angular roadmap.

Detecting @​Input changes in Angular with ngOnChanges and Setters

In this post you’ll learn how to detect changes to an @Input property in Angular. We’ll explore both using ngOnChanges lifecycle hook and also more native TypeScript features such as set (also known as a setter).

The first question is “how do we detect a change to an input?”. This, of course, comes with multiple answers.

Let’s start with the Angular way, which is using the dedicated ngOnChanges lifecycle hook.

I’ve created this example for you to explore before we begin so you can see how the radio buttons emit change events, which then use combineLatest to pass the selected player down into the <selected-player> component via a player property:

Let’s take a quick look at the <selected-player> component:

import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
import { Player } from './player.model';

@Component({
  selector: 'selected-player',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: ` <h1>Hello {{ player.name }}!</h1> `,
})
export class SelectedPlayerComponent {
  @Input()
  player: Player;
}

As you can see from the component definition, we’re using ChangeDetectionStrategy.OnPush, indicating we’re disabling Angular’s change detection and only forcing a re-render whenever a property changes. But - what if we want to know when the property has changed?

This would open up many possibilities for us to explore and intercept the data that’s coming in.

NgOnChanges

Let’s implement the OnChanges lifecycle hook and bring out the console.log:

import {
  Component,
  Input,
  OnChanges,
  SimpleChanges,
  ChangeDetectionStrategy,
} from '@angular/core';
import { Player } from './player.model';

@Component({...})
export class SelectedPlayerComponent implements OnChanges {
  @Input() player: Player;

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

ngOnChanges gives us a changes object through the function arguments, which is of type SimpleChanges.

Angular Directives In-Depth eBook Cover

Free eBook

Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.

  • Green Tick Icon Observables and Async Pipe
  • Green Tick Icon Identity Checking and Performance
  • Green Tick Icon Web Components <ng-template> syntax
  • Green Tick Icon <ng-container> and Observable Composition
  • Green Tick Icon Advanced Rendering Patterns
  • Green Tick Icon Setters and Getters for Styles and Class Bindings

It’s an object made up of each input property and contains previous and next values of each property.

Our changes object would contain this:

{
  player: {
    firstChange: true, // is this the first time it's changed?
    previousValue: undefined, // it's the first change, so there was no previous value
    currentValue: { id: 3, name: 'Toad' } // here's the new value that's changed
  }
}

Our @Input() player: Player then becomes a property inside this changes object, with each value being of type SimpleChange (SimpleChange, not SimpleChanges). A SimpleChange object has the firstChange, isFirstChange(), previousValue and currentValue properties and methods. You’ll get a SimpleChange object for each @Input() on the component.

Getting back to business, let’s explore when the ngOnChanges lifecycle hook fires and use it to detect when the input property changes.

To get the currentValue from our changes object we can then do this:

@Component({...})
export class SelectedPlayerComponent implements OnChanges {
  @Input()
  player: Player;

  ngOnChanges(changes: SimpleChanges) {
    console.log(changes.player.currentValue);
  }
}

You can see that there’s now { id: 3, name: 'Toad' } in the console:

Why is it logged already? That’s because ngOnChanges is called by Angular as the component is instantiated. It is also invoked before ngOnInit in case you were unaware.

So, what about when the input changes? We don’t want to necessarily run some logic after the component has been created do we? That’s why firstChange and isFirstChange() exist:

@Component({...})
export class SelectedPlayerComponent implements OnChanges {
  @Input()
  player: Player;

  ngOnChanges(changes: SimpleChanges) {
    if (!changes.player.firstChange) {
      // only logged upon a change after rendering
      console.log(changes.player.currentValue);
    }
  }
}

Now you’ll see nothing in the console until you select a new player:

🍾 Use ngOnChanges when you are likely dealing with multiple properties changing at once, or wish to do something with multiple properties. It can be easier to reason with a setter/getter, but it’s good to know the ngOnChanges solution exists.

Also, we’re yet to discuss private properties - of which we can encapsulate fully with setters and getters!

So, that’s how to utilize ngOnChanges to detect when an input property has changed, so let’s check out set alongside @Input()!

Setters and Getters

Did you know you can use set alongside an @Input()? No? Then prepare your mind to be blown!

We can ditch OnChanges, SimpleChanges, SimpleChange and friends and opt for a simpler approach that uses a single setter and getter on an input property.

Not only this, but we can provide full private properties and use a getter to retrieve them (for displaying in the view).

Here’s how we can rewrite our component to use a set property with player instead:

@Component({...})
export class SelectedPlayerComponent {
  @Input()
  set player(value: Player) {
    console.log(value);
  }
}

Now, any time the player property changes we’ll get notified straight away through the set function. The set will be called everytime the player property changes!

🤔 Unsure what a setter and getter really do? Read my deep dive on setters and getters in TypeScript to uncover more!

But - this set doesn’t do much on it’s own it only allows us to see the new value:

How can we then use a private property to hide the value and also display it in the component’s view? Alongside a set, we’ll introduce a get:

import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
import { Player } from './player.model';

@Component({
  selector: 'selected-player',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: ` <h1>Hello {{ player?.name }}!</h1> `,
})
export class SelectedPlayerComponent {
  private _selected: Player;

  @Input()
  set player(value: Player) {
    this._selected = value;
  }

  get player(): Player {
    return this._selected;
  }
}

Here’s the output for you with the set and get implemented, notice how the get simply returns the this._selected private property:

Fact: a set will always be invoked before the ngOnChanges lifecycle hook. This is because it’s “plain JavaScript” (well, TypeScript).

Breaking @Input() references

There’s always a potential to “change” local state inside your component without propagating the change up to the parent. Angular uses one-way dataflow for a reason.

Data comes down and events go up.

When you’re ready to commit a state change, you’ll emit an event via an @Output and handle the change in a “smart component”.

Objects and Arrays in JavaScript are passed by reference, which means if you start to change their properties inside a component, the change will likely reflect “upwards”. This is not two-way data binding but the normal behavior of JavaScript.

We can get around this by “cloning” the incoming property values. For example, to complete “break the chain” from parent to child component we could use our set to spread the object into a new object (which gives our internal component a new object to mutate, and when we’re ready we can emit those changes back to the parent):

import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
import { Player } from './player.model';

@Component({
  selector: 'selected-player',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: ` <h1>Hello {{ player?.name }}!</h1> `,
})
export class SelectedPlayerComponent {
  private _selected: Player;

  @Input()
  set player(value: Player) {
    this._selected = { ...value };
  }

  get player(): Player {
    return this._selected;
  }
}

We could then change the local properties and keep some local state, for instance updating the this._selected.name property. This would then not affect the data in the parent component, as we’ve essentially broken the link between the references by cloning.

If you are serious about your Angular skills, your next step is to take a look at my Angular courses where you’ll learn Angular, TypeScript, RxJS and state management principles from beginning to expert level.

Before using the spread operator ... we would have used Object.assign({}, value).

And there you have it! Many ways to detect incoming property changes within your components, and the knowledge to handle multiple or single properties, and also clone the objects/arrays to break the references.

Learn Angular the right way.

The most complete guide to learning Angular ever built.
Trusted by 82,951 students.

Todd Motto

with Todd Motto

Google Developer Expert icon Google Developer Expert

Related blogs 🚀

Free eBooks:

Angular Directives In-Depth eBook Cover

JavaScript Array Methods eBook Cover

NestJS Build a RESTful CRUD API eBook Cover