Angular has had a built-in way to wait on your functions to load, and you didn't know about it!
In the past...
You needed to import the hidden function...
import { ɵPendingTasks as PendingTasks } from '@angular/core';
Notice the greek letter that you wouldn't normally find with autocomplete.
Today
It is experimental, but you will soon be able to just import PendingTasks
.
import { ExperimentalPendingTasks as PendingTasks } from '@angular/core';
Setup
I use my useAsyncTransferState
function for hydration. This ensures an async call, a fetch in this case, only runs once, and on the server.
export const useAsyncTransferState = async <T>(
name: string,
fn: () => T
) => {
const state = inject(TransferState);
const key = makeStateKey<T>(name);
const cache = state.get(key, null);
if (cache) {
return cache;
}
const data = await fn() as T;
state.set(key, data);
return data;
};
Token
We need reusable tokens for the REQUEST
object.
// request.token.ts
import { InjectionToken } from "@angular/core";
import type { Request, Response } from 'express';
export const REQUEST = new InjectionToken<Request>('REQUEST');
export const RESPONSE = new InjectionToken<Response>('RESPONSE');
We must pass the request object as a provider in our render function.
// main.server.ts
export default async function render(
url: string,
document: string,
{ req, res }: { req: Request; res: Response }
) {
const html = await renderApplication(bootstrap, {
document,
url,
platformProviders: [
{ provide: REQUEST, useValue: req },
{ provide: RESPONSE, useValue: res },
],
});
return html;
}
Angular is currently in the process of adding all of these features, and potentially endpoints!!!!! 😀 😀 😀 🗼 🎆
Fetch Something
Because endpoints are not currently there, I am testing this with Analog. Here is a hello
endpoint that takes 5 seconds to load.
import { defineEventHandler } from 'h3';
export default defineEventHandler(async () => {
const x = new Promise((resolve) => setTimeout(() => {
resolve({
message: "loaded from the server after 5 seconds!"
});
}, 5000));
return await x;
});
Test Component
Here we use the request
in order to get the host URL. Then we use useAsyncTransferState
to ensure things only run on the server, and only once. Finally, we use pendingTasks
to ensure the component is not fully rendered until the async completes.
import { AsyncPipe } from '@angular/common';
import {
Component,
ExperimentalPendingTasks as PendingTasks,
inject,
isDevMode
} from '@angular/core';
import { REQUEST } from '@lib/request.token';
import { useAsyncTransferState } from '@lib/utils';
@Component({
selector: 'app-home',
standalone: true,
imports: [AsyncPipe],
template: `
<div>
<p class="font-bold">{{ data | async }}</p>
</div>
`
})
export default class HomeComponent {
private pendingTasks = inject(PendingTasks);
protected readonly request = inject(REQUEST);
data = this.getData();
// fetch data, will only run on server
private async _getData() {
const schema = isDevMode() ? 'http://' : 'https://';
const host = this.request.headers.host;
const url = schema + host + '/api/hello';
const r = await fetch(url, {
headers: {
'Content-Type': 'application/json',
}
});
const x = await r.json();
return x.message;
}
// fetch data with pending task and transfer state
async getData() {
const taskCleanup = this.pendingTasks.add();
const r = await useAsyncTransferState('pending', async () => await this._getData());
taskCleanup();
return r;
}
}
Pending Task
Pending Task is very simple.
// create a new task
const taskCleanup = this.pendingTasks.add();
// do something async
const r = await fn();
// let Angular know it can render
taskCleanup();
Thats it! Bingo Shabongo!
Repo: GitHub
Demo: Vercel Edge - Takes 5s to load!
Should you use this?
Nope! Seriously, don't use this.
After going down the rabbit hole for years on Angular async rendering (read the old posts in this chain), it is definitely best practice to put ALL async functions in a resolver
. The resolver MUST load before a component, which is a much better development environment. The only exception would be @defer
IMHO.
However, there are some edge cases where it makes sense for your app. The is particularly evident when you don't want to rewrite your whole application to use resolvers. Either way, you need to be aware of your options!
J