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);
}
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);
});
}
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;
});
}
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);
}
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