Forcing Angular SSR to Wait in 2024

Jonathan Gamble - Jul 10 - - Dev Community

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';
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;

});
Enter fullscreen mode Exit fullscreen mode

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

}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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

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