Async pipe causes setters to fire even when input value hasn't changed
See original GitHub issueI’m submitting a…
[ ] Regression (a behavior that used to work and stopped working in a new release)
[x] Bug report
[x] Performance issue
[ ] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question
[ ] Other... Please describe:
Current behavior
Accessing deeply nested object properties when binding to an input works as expected with simple values:
<hello [name]="nonAsyncNames.name"></hello>
setInterval(() => {
this.nonAsyncNames = {name: 'some string'};
}, 1000);
As expected, the setter
for the name
property in the Hello
component will only be called once.
// this will only be called once
@Input() set name(val: string) {
this._name = val;
console.log("name set, new val:", val)
};
Even though the reference to the nonAsyncNames
object changes, the value of the final property name
doesn’t - so it works perfectly.
On the other hand, taking the same example with an async
pipe, we get different behavior:
<hello [name]="(asyncNames | async).name"></hello>
asyncNames = interval(1000).pipe(
map(x => ({name: 'some string'}))
);
In HelloComponent.ts
:
// called MULTIPLE times
@Input() set name(val: string) {
this._name = val;
console.log("name set, new val:", val)
};
The setter in the component is called multiple times.
Expected behavior
Unless I am missing something, it seems that both examples should behave in the same way: the child setter should only get called if the new input is different from the previous one.
I’ve managed to fix it for now by just creating another observable that emits name
directly:
asyncNames.map(x => x.name)
and then using the async
pipe on it directly. But I’m still wondering if by default both examples should be the same.
In real-world cases, this might cause performance issues if more expensive logic is executed in the setter. In the following example, this might cause issues:
//NgRx redux state
const weatherState= {
season: { /* ... */ }.
wind: { windSpeed: 10 }
}
<season-label [name]="(state | async).season.name"></season-label>
As we’re using Redux, any time any change to the windSpeed
happens, it will create a new instance of weatherState
. That means any time windSpeed
changes (often), it would cause the setter of the season-label
component to fire as well - something that might not be expected, as seasons
change much less often than wind speed.
Minimal reproduction of the problem with instructions
I’ve created this stackblitz with the above examples: https://stackblitz.com/edit/angular-v3mqed?file=src%2Fapp%2Fapp.component.ts
What is the motivation / use case for changing the behavior?
Environment
Angular version: 6.0.0
Browser:
- [x] Chrome (desktop) version 68.0.3440.106
- [ ] Chrome (Android) version XX
- [ ] Chrome (iOS) version XX
- [ ] Firefox version XX
- [ ] Safari (desktop) version XX
- [ ] Safari (iOS) version XX
- [ ] IE version XX
- [ ] Edge version XX
For Tooling issues:
- Node version: XX
- Platform:
Others:
Issue Analytics
- State:
- Created 5 years ago
- Reactions:1
- Comments:8 (4 by maintainers)
Top GitHub Comments
@rarmatei … I am still not sure what should be the final decision. As you could see even Miško answer wasn’t really satisfying because of missing argumentation why exactly is the behavior expected. There is even identified the part of the code which looks to be responsible for that
a bit strange
behavior. I personally still think that there is a space how to improve the code.This has been fixed in Angular 10 as the
async
pipe has stopped usingWrappedValue
in #36633. The wrapped value would force a dirty binding, causing the setters to be invoked.