DEV Community

Cover image for Forcing Angular to Wait on Your Async Function
Jonathan Gamble
Jonathan Gamble

Posted on • Edited on

Forcing Angular to Wait on Your Async Function

Update 7/9/24

I highly recommend you just use a resolver to load your async data there; it is by far the easiest and best practice. However, PendingTasks is probably better for you in 2024 if you must do it in a component.


Original Post


For SSR and node.js usage of Angular, we may need to have a Promise complete before the page is loaded. This is especially true when we need to create meta tags for SEO. Yes, our app is slower, but we have to get it indexable.

Every wonder why your meta tags seem to work sometimes, but not other times? It is because ngOnInit is NOT an async function, even with async, neither is a constructor which must return this, and neither is an async pipe in your template. Sometimes the fetches return on time, other times they don't. So, I added this post:


ngOnInit does NOT wait for the promise to complete. You can make it an async function if you feel like using await like so:

import { take } from 'rxjs/operators';

async ngOnInit(): Promise<any> {
  const data = await this.service.getData().pipe(take(1)).toPromise();
  this.data = this.modifyMyData(data);
}
Enter fullscreen mode Exit fullscreen mode

However, if you're using ngOnInit instead of the constructor to wait for a function to complete, you're basically doing the equivalent of this:

import { take } from 'rxjs/operators';

constructor() {
  this.service.getData().pipe(take(1)).toPromise()
    .then((data => {;
      this.data = this.modifyMyData(data);
    });
}
Enter fullscreen mode Exit fullscreen mode

It will run the async function, but it WILL NOT wait for it to complete. If you notice sometimes it completes and sometimes it doesn't, it really just depends on the timing of your function.

Using the ideas from this post, you can basically run outside zone.js. NgZone does not include scheduleMacroTask, but zone.js is imported already into angular.

Solution

import { isObservable, Observable } from 'rxjs';
import { take } from 'rxjs/operators';

declare const Zone: any;

async waitFor<T>(prom: Promise<T> | Observable<T>): Promise<T> {
  if (isObservable(prom)) {
    prom = firstValueFrom(prom);
  }
  const macroTask = Zone.current
    .scheduleMacroTask(
      `WAITFOR-${Math.random()}`,
      () => { },
      {},
      () => { }
    );
  return prom.then((p: T) => {
    macroTask.invoke();
    return p;
  });
}
Enter fullscreen mode Exit fullscreen mode

I personally put this function in my core.module.ts, although you can put it anywhere.

Use it like so:

constructor(private cm: CoreModule) {
  const p = this.service.getData();
  this.post = this.cm.waitFor(p);
}
Enter fullscreen mode Exit fullscreen mode

You could also check for isBrowser to keep your observable, or wait for results.

Conversely, you could also import angular-zen and use it like in this post, although you will be importing more than you need.

I believe this has been misunderstood for a while now, so I hope I am understanding this correctly now.

I should also add you don’t always want to do this if you’re app loads in time without it. Basically you’re app is faster without it using simultaneous loading, but a lot of the time we have to have it. For seo, do html testing to make sure it loads as expected every time.

Let me know if it solves your problem.

Here is my stackoverflow post on this.

J

Top comments (7)

Collapse
 
titomoi profile image
TitoMoi

Another example, in this case to block the template render is:

//myclass.component.ts

class myClass() ... {

 data = undefined;

  async ngOnInit() {
    this.data = await myService.myFunction();
  }
}
Enter fullscreen mode Exit fullscreen mode

//myclass.component.html

<ng-template *ngIf="data">
...
</ng-template>
Enter fullscreen mode Exit fullscreen mode

or use resolvers, but I think this is less code and less hard.

Collapse
 
jdgamble555 profile image
Jonathan Gamble

That will not always work. ngOnInit is NOT an async function even with async, which is why I wrote this article. It will NOT wait for the function to complete. It just allows you to use the await keyword, but it is not an async function, despite having the async keyword. The function may or may not complete in time.

Collapse
 
titomoi profile image
TitoMoi

true, is not async in fact, but this works because await is queuing the call, is like:

ngOnInit()
... some time later...
myFunction resolves and data is populated, so the template is unblocked.

This is like a "hack" and confusing but works because the template has to wait the data.

This scenario is not a default one, blocking the template is not a good practice but in some scenario like the init of an app maybe fits and is not necessary to use Factories.

Thread Thread
 
jdgamble555 profile image
Jonathan Gamble • Edited

The problem is that it is not guaranteed to be resolved before the component is initialized because it is not a REAL async function. Using zone.js will guarantee it gets resolved before the component is loaded. Any other function with async in front of it, IS a real async function. Yes, it is a hack, but sometimes there is no other way.

Collapse
 
ekyu88 profile image
Ed

Can you post the code for 'firstValueFrom(prom)'? Also what typescript version is this? Thanks.

Collapse
 
eliejosaber profile image
ElieJoSaber

this.post = this.cm.waitFor(p);
All good thanks, just we should put await here:
this.post = await this.cm.waitFor(p);

Collapse
 
heinrichbrandes profile image
Walter Hassain

Does this work with change detection onPush?