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
Top comments (7)
Another example, in this case to block the template render is:
//myclass.component.ts
//myclass.component.html
or use resolvers, but I think this is less code and less hard.
That will not always work.
ngOnInit
is NOT an async function even withasync
, which is why I wrote this article. It will NOT wait for the function to complete. It just allows you to use theawait
keyword, but it is not anasync
function, despite having theasync
keyword. The function may or may not complete in time.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.
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.Can you post the code for 'firstValueFrom(prom)'? Also what typescript version is this? Thanks.
this.post = this.cm.waitFor(p);
All good thanks, just we should put await here:
this.post = await this.cm.waitFor(p);
Does this work with change detection onPush?