11

Data bindings don't get updated if their values are changed after an await statement.

  handle() {
    this.message = 'Works'
  }

  async handle() {
    this.message = 'Works'
  }

  async handle() {
    await new Promise((resolve, reject) => {
      resolve()
    })
    this.message = 'Works'
  }

  async handle() {
    await new Promise((resolve, reject) => {
      setTimeout(() => resolve(), 3000)
    })
    this.message = 'Doesn\'t work'
  }

  handle() {
    new Promise((resolve, reject) => {
      setTimeout(() => resolve(), 3000)
    })
    .then(() => this.message = 'Works')
  }

Why do the last two not behave the same? aren't they supposed to be the same thing?

Ionic: 3.9.2

Angular: 5.0.3

TypeScript: 2.4.2

EDIT: I came across another problem with this which may be useful to some.

Changing the values of a binding in the constructor behaves differently to ionViewDidLoad or ngOnInit!

  constructor(private zone: NgZone) {
    // This will cause the same problems, bindings not updating
    this.handle()
  }

  constructor(private zone: NgZone) {
    // Unless you do this...
    this.zone.run(() => {
      this.handle()
    })
  }

  ionViewDidLoad() {
    // But I think this is better/cleaner
    this.handle()
  }
ovg
  • 1,486
  • 1
  • 18
  • 30
  • https://stackblitz.com/edit/angular-x8szh6 Seems good to me. – Jeto Mar 14 '18 at 17:36
  • @Jeto It works with console.log, the problem is that if you try it in a component with data binding, the value doesn't get updated – ovg Mar 14 '18 at 17:53
  • Can you build a minimal StackBlitz from it then? – Jeto Mar 14 '18 at 17:54
  • I'm trying, haven't heard of it before :) – ovg Mar 14 '18 at 17:55
  • I can't seem to reproduce it there... My packages are different versions and I cannot set a tsconfig either. – ovg Mar 14 '18 at 18:14
  • Could this be a transpiler problem? – ovg Mar 14 '18 at 18:15
  • I think your problem is not Angular itself see here https://stackblitz.com/edit/angular-s3eweu?file=app%2Fapp.component.html – CREM Mar 14 '18 at 18:51
  • @CryingFreeman It seems that way given it works on stackblitz, but when I compile in my env and run in the same browser it doesn't – ovg Mar 14 '18 at 18:54

4 Answers4

14

Angular relies on Zone.js for change detection, and Zone.js provides this by patching every API that can provide asynchronous behaviour.

The problem is in how native async functions are implemented. As confirmed in this question, they don't just wrap around global Promise but rely on internal mechanisms that may vary from one browser to another.

Zone.js patches Promise but it's impossible to patch internal promise that is used by async functions in current engine implementations (here is open issue for that).

Usually (async () => {})() instanceof Promise === true. In case of Zone.js, this isn't true; async function returns an instance of native Promise, while Promise global is zone-aware promise patched by Zone.js.

In order to make native async functions work in Angular, change detection should be additionally triggered. This can be done by triggering it explicitly (as another answer already suggests) or by using any zone-aware API. A helper that wraps async function result with zone-aware promise will do the trick:

function nativeAsync(target, method, descriptor) {
  const originalMethod = target[method];
  descriptor.value = function () {
    return Promise.resolve(originalMethod.apply(this, arguments));
  }
}

Here is an example that uses @nativeAsync decorator on async methods to trigger change detection:

  @nativeAsync
  async getFoo() {
    await new Promise(resolve => setTimeout(resolve, 100));
    this.foo = 'foo';
  }

Here is same example that doesn't use additional measures to trigger change detection and expectedly doesn't work as intended.

It makes sense to stick to native implementation in environment that doesn't require transpilation step. Since Angular application is supposed to be compiled any way, the problem can be solved by switching from ES2017 to ES2015 or ES2016 TypeScript target.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • can you please provide an example of this working on an Angular app? It would help a lot :) – jbarradas Jul 06 '18 at 16:14
  • 1
    @jbarradas Updated [the plunk](http://plnkr.co/edit/lSZgKYybrc0s6sAVRivd?p=preview) in the answer to work because of breaking changes in RxJS 6. – Estus Flask Jul 06 '18 at 17:43
6

just like estus said, currently zone.js don't support native async/await, so you can't compile typescript which target to ES2017, and I am working on it, https://github.com/angular/zone.js/pull/795, I have made a working demo which can run in nodejs, but in browser(chrome), it will still take some time because chrome does't open javascript version of AsyncHooks and PromiseHooks for now.

jiali passion
  • 1,691
  • 1
  • 14
  • 19
2

Check your browser version and what it supports, async/await is native with ES2017. If your browser doesn't support that use ES2016 as your target.

I needed ES2016 for Electron.

ovg
  • 1,486
  • 1
  • 18
  • 30
  • Answering my own question - although I can't accept it as the answer for another two days... – ovg Mar 14 '18 at 22:55
1

It's got to do with how change detection works in Angular. See: https://stackblitz.com/edit/angular-jajbza?file=app%2Fapp.component.ts

I'm betting Ionic uses OnPush strategy by default, or you've enabled it, as I did in the Blitz. It's all good, IMO it should be on by default anyway as it forces you to think about these things and write more performant code.

Can't say exatly why your view is updated in your last example when you call .then though. Maybe in that case CD manages to keep track of the Promise, but not with async functions. Async functions themselves return a Promise if you don't, so basically after transpilation, async handle() returns something like Promise.resolve(null), though the actual JS code probably looks messier than that.

Edit: Another way, perhaps cleaner than calling detectChanges manually would be to run anything that changes the view inside Angular's zone:

import { NgZone } from '@angular/core';
constructor (private zone:NgZone){}

// after await somePromise :
this.zone.run(() => { 
    this.someProperty = 'Something changed';
});

Edit2: Interesting, zone.run() actually doesn't change anything. Tested that in the blitz. So manual CD is the only way. One more reason to avoid async/await and try to stick with Observables and NG's async pipe. :)

funkizer
  • 4,626
  • 1
  • 18
  • 20
  • Right now I'll have to revert my async/await code to promises in order to not have to do these types of things. Although everything fully works here: https://stackblitz.com/edit/ionic-g8d9fu but it won't work in my environment for some reason... I even explicitly set changeDetection: ChangeDetectionStrategy.Default. Is there any way I can get it working in my environment? – ovg Mar 14 '18 at 18:36
  • I think it may be a transpilation issue – ovg Mar 14 '18 at 18:36
  • It's something to do with Ionic. I haven't used it but heard it cause OnPush -like effects when the default strategy is used. Also happened to me with Electron, so something about those environments causes change detection to 'not just work' sometimes. If I were you, I'd just add cdRef.markForCheck() in those async functions to get it working quickly. =) – funkizer Mar 14 '18 at 18:44
  • .. markForCheck() instead of detectChanges() even though it may cause more overhead, because detectChanges can cause "you tried to something something after view has been destroyed" if you happen to call it after a component has been destroyed in the meantime. So it's safer :) – funkizer Mar 14 '18 at 18:45
  • Funny, I'm actually using electron – ovg Mar 14 '18 at 18:47
  • Oh, i donno why i assumed it was like cordova etc :D so that's it – funkizer Mar 14 '18 at 18:50
  • And i'm remembering more clearly now, i was also using async await a lot those days. These days i don't touch them, observables all the way :) – funkizer Mar 14 '18 at 18:52
  • Actually I just tested in Vivaldi browser and it behaves the same :( – ovg Mar 14 '18 at 18:52
  • It can be very difficult to conditionally control flow with promises which is why I was giving async/await a go, but maybe Observables are best/more powerful... but if all I want to do is use if statements to check boolean values returned from async/await, Observables are overkill and more difficult to read for somebody who is not so used to them – ovg Mar 14 '18 at 18:58
  • True, async/await imakes code nicer to read. Btw. try adding async to the greet -method here and see what happens on the right side :D http://www.typescriptlang.org/play/ – funkizer Mar 14 '18 at 19:02
  • So yeah, it seems all that extra stuff runs outside of Angular zone no matter what you do. – funkizer Mar 14 '18 at 19:04
  • The code on stackblitz works fine on my browser, but my compiled code doesn't, it's not a support issue – ovg Mar 14 '18 at 19:05
  • Doesn't work even with detectChanges() ? Yeah I deleted that stuff about Chrome as soon as I typed it - supporting async natively does nothing as the same transpiled code is run in every browser :) – funkizer Mar 14 '18 at 19:07
  • ya it works with that, but I'm talking about my original code – ovg Mar 14 '18 at 19:09
  • ah ok, mother of god what is Typescript doing to my code =( no wonder it doesn't work – ovg Mar 14 '18 at 19:10
  • hehe :P yeah. This is one reason it may be best to avoid asyncawait until there's no need to transpile it – funkizer Mar 14 '18 at 19:11
  • yeah, after seeing that I don't even want to try to fix it anymore, thanks – ovg Mar 14 '18 at 19:14
  • I just tried swapping my target from es2017 to es2016 and it worked https://pastebin.com/SResRJmY – ovg Mar 14 '18 at 19:30