Forcing Angular to Wait on Your Async Function

Jonathan Gamble - Oct 7 '21 - - Dev Community

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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .