question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Async pipe causes setters to fire even when input value hasn't changed

See original GitHub issue

I’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:closed
  • Created 5 years ago
  • Reactions:1
  • Comments:8 (4 by maintainers)

github_iconTop GitHub Comments

3reactions
mlc-mlapiscommented, Sep 14, 2018

@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.

1reaction
JoostKcommented, May 13, 2021

This has been fixed in Angular 10 as the async pipe has stopped using WrappedValue in #36633. The wrapped value would force a dirty binding, causing the setters to be invoked.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Detecting @​Input changes in Angular with ngOnChanges ...
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 ......
Read more >
Angular 4 - html input element change event triggers @Input ...
1 Answer 1 · when a DOM event the component listens to was received · when the |async pipe receives a new event...
Read more >
How to detect when an input() value changes in Angular
Detect when input value changes in Angular · 1. Using the ngOnChanges() lifecycle method · 2. Using Input Setter and Getter Property ·...
Read more >
Angular OnPush Change Detection - Avoid Common Pitfalls
As we can see, the component now has an input property which is an Observable, that gets subscribed to via the async pipe....
Read more >
3 Ways to Pass Async Data to Angular 2+ Child Components
ngOnChanges is a lifecycle hook that run whenever it detects changes to input properties. That means it's guaranteed that everytime data input ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found