Why Angular Input Setter is Only Being Fired Once

Why Angular Input Setter is Only Being Fired Once

Sunny Sun Lv4

Examine various approaches to pass data between components

Working on an Angular App requires composing different components and passing data between them. A common approach to passing data from the parent to child components is using the input property setter. We all know getter and setter. They are simply for intercepting access to the property.

Surprisingly, there is a subtle difference in Angular component input setter behavior compared with the “standard” TypeScript setter. I found it when working on the following issue.

Original problem

To illustrate the issue, I set up a contrived example as below.

1
2
3
4
5
6
7
8
9
10
// Parent Component  
<button (click)="reset()">set to Parent</button>
<app-child [data]="message"></app-child>reset() {
this.message = 'Parent';
}//Child Component
<p>Value is: {{ data }}</p>_val = '';
@Input()
set data(val: string) {
this._val = val;
}

In the above example, we have two components: parent and child. data is a simple input setter in the child component. When a user clicks on the “set to Parent” button from the parent component, a string value will be set to “Parent,” and the child component setter will be invoked.

Actually, the above statement is only true for the first time. If you click on the “set to Parent” button multiple times, the child component setter won’t be triggered from 2nd time onward.

Thus, it looks like the input setter is only fired once if the value of the setter hasn’t been changed.

Root cause

I dig inside the angular source code to understand the root cause of this behavior. In the following [bindingUpdated](https://github.com/angular/angular/blob/d1ea1f4c7f3358b730b0d94e65b00bc28cae279c/packages/core/src/render3/bindings.ts#L52) function line 7, Angular checks value change by comparing the new value with the old value in its component’s logical view. It only updates binding when the value actually changes.

The Angular Input setter is designed to ignore the binding update if the value hasn’t changed. In contrast, the setter in a normal TypeScript class will be triggered upon every invocation regardless of whether the value changed.

For this use case, I want to trigger the reset of the child component whenever the “set to Parent” button in the parent is clicked. How can I achieve this?

Workaround

The first workaround I tried was to use ngOnChanges. I added the following code into the child component and hope it will capture the data when the “set to Parent” button is clicked.

1
2
3
ngOnChanges(changes: SimpleChanges) {  
console.log('ngOnChanges', changes);
}

But the console logs only print the first time the button clicks. Obviously, the underlined binding code for ngOnChanges is the same as the Input setter.

The second attempt is to expose a new public method setParent in the child component. From the parent component, we create a reference to the child component with ViewChilddecorator and invoke the setParent method upon the button clicking.

1
2
3
4
5
6
7
8
9
10
11
12
// Child Component  
setParent() {
this.data = 'Parent';
}
// Parent Component
@ViewChild('childComp', { static: true })
childComp: ChildComponent;

resetChild() {
this.childComp.setParent();
}// Parent Component template
<button (click)="resetChild()">call reset to Parent</button>

This approach works, but I am not very satisfied with it. Exposing a method and directly calling another component is not a clean way. It creates a tight coupling between the two components, which will get worse when the method is consumed by more than one parent component.

I still prefer to use the input property, as it creates a contract between components. Can we still use the input property setter by working around the restriction?

A better approach using observable

If we go back to the Angular source code, it uses [Object.is](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) to detect value changes.

1
If(Object.is(oldValue, value)) 

When comparing two objects, Object.is will return true if “both the same object (meaning both values reference the same object in memory)”.

Thus, if we pass into the input property with an observable object that contains the same value, Angular will take it as a changed value!

Below is our new implementation using observable as an Input property.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Parent Component  
eventsSubject: Subject<string> = new Subject();resetByObservable() {
this.eventsSubject.next('Parent');
}// Parent Component template
<button (click)="resetByObservable()">set to Parent by observable</button><app-childObservable [events]="eventsSubject"></app-childObservable>// Child Component
_val: Subject<string> = new Subject();
@Input()
set events(val: Subject<string>) {
this._val = val;
}
ngOnInit() {
this.eventsSubscription = this.events.subscribe((x) => {
this.data = x;
});
}

In the above implementation, we use an Input setter as the Subject type and subscribe to the data change in the child component. The child component subscription captures every click of the “set to Parent” button.

You can try out the above example at the stackblitz project.

Input property setter vs. ngOnChanges

In the previous sections, we tried the Input property setter and ngOnChangeslife cycle hook.

They are two ways to detect and react to data changes, and can be used to pass parameters between components. Both will only be triggered if the binding value changes.

They can be used in different use cases. With ngOnChanges you have access to all property changes in one place. For Input property setters, it only applies to a single property.

There are other common ways to share data between components

  • via shared service with BehaviorSubject
  • via the @Output decorator and EventEmitter from a child or sibling component

Summary

Although the property setter in TypeScript is simple and straightforward, the devil is in the details: there is a subtle difference for the Angular input property setter on change detection. This article explores resolving the issue of sending repeated values between two Angular components.

There are different ways we can choose to pass data between components. Using @Input and @Output is my preferred method, as they serve as a contract between components and make the components more reusable.

If you like this article, you may also like to read my other Angular article below.

Angular State Management with Observable Service Pattern

Angular state management is the core of any Angular App, but there is no one-size-fit-all solution.

javascript.plainenglish.io

I hope you find this article useful. You can find the sample code in this stackbliz project.

  • Title: Why Angular Input Setter is Only Being Fired Once
  • Author: Sunny Sun
  • Created at : 2022-01-27 00:00:00
  • Updated at : 2024-08-16 19:46:32
  • Link: http://coffeethinkcode.com/2022/01/27/why-angular-input-setter-is-only-being-fired-once/
  • License: This work is licensed under CC BY-NC-SA 4.0.