[Angular] Stop using setter for Input or..

kelly woo
3 min readJan 13, 2022

--

Photo by JESHOOTS.COM on Unsplash
@Input('matBadgeColor') get color(): ThemePalette {    
return this._color;
}
set color(value: ThemePalette) {
this._setColor(value);
this._color = value;
}

This snippet is from material ui, famous best practice library of angular and they use setter and what is wrong to use it?, you might ask the question.
Actually using setter is not a problem, on the contrary it is convenient and simple you can validate the data and keep the prop private, also easy to see.

The problem is people who uses setter for ngOnChanges.

ngOnChanges is the lifecycle hook to notify the component of the change of props.
The object represents the change of props is poorly typed and has every changes in it , so performing some actions on props requires validation of this prop before.

ngOnChanges(changes: SimpleChanges){
if(changes['color']){
....
}
} // It is poorly typed, and typescript forces me to use index key rather than property accessor.

This is not to blame using setter on Input at all, it is reminder that you should know when to use setters and when to not.
I’ve seen many code like this.

@Input() set a(v){
this._a = v;
this.callback();
}
@Input() set b(){
this._b = b;
this.callback();
}
callback(){
this.filterBy(this._a, this_b);
}

With this code, you called callback twice for one change cycle. This is very much simplified to explain what is happening, but in complex code, this happens a lot!!

Setter calls when the property sets and ngOnChanges calls once all properties got updated, if the callback only concern one prop(yes, most time goes here), no problem, but otherwise you’ve wasted the call.

But this is not the real problem, only weakening the performance, real devil is behind to crash the app.

Here’s simple app, by toggling the button, display filtered menu by drink or snack. See the code below.

type MenuType = 'drink' | 'snack';
interface Menu {
name: string;
cate: MenuType;
}
@Component({
selector: 'child',
template: `<div><strong>{{_cate}}</strong>:
<ul>
<li *ngFor="let item of filteredMenu">
<span>{{item.name}}</span>
</li>
</ul>
</div>`,
})export class ChildComponent {
private _menu;
public _cate;
public filteredMenu;
@Input() set menu(value: Menu[]) {
this._menu = value;
}
@Input() set cate(value: MenuType) {
this._cate = value;
this.filterMenu();
}
filterMenu() {
this.filteredMenu = this._menu.filter((m) => m.cate === this._cate);
}
}
@Component({
selector: 'my-app',
template: `<child [menu]="menu" [cate]="cate"></child><button type="button" (click)="click()">update</button>`,
})
export class AppComponent {
cate: MenuType = 'drink';
menu: Menu[] = [
{ name: 'coffee', cate: 'drink' },
{ name: 'latte', cate: 'drink' },
{ name: 'cookie', cate: 'snack' },
{ name: 'macaron', cate: 'snack' },
{ name: 'cake', cate: 'snack' },
];
click() {
this.cate = this.cate === 'drink' ? 'snack' : 'drink';
}
}

You see any problem here ?
Because menu is provided at the construct time, only change of ‘cate’ prop calls filtering. At first glance, it makes sense.
But no, it does not work fine.
Setter calls the moment value sets , once you change the input order in app template reversed, the app will crash hard because the time cate is set, menu is undefined.. so props injected with setter should consider the matter of order.

<child [cate]="cate" [menu]="menu"></child>
<child [menu]="menu" [cate]="cate"></child>
// not same if you use setter

You might think you won’t make this mistake but remember, this is simplified version. I’ve seen many juniors making this kind of mistakes. So app does not fail on their turn, but others change some, yes it collapses.(or if you guarded it with the default value, it may needs some changes to render the right data.)

You might think one is fine, but then there will be adding another and there will be other Inputs associating with the existing props..

So please, stop using setter with operation calls please handle it in ngOnChanges.

Thank you.

--

--