How to Properly Fetch Data in Angular Universal

Jonathan Gamble - Feb 12 '22 - - Dev Community

After building a few apps in SvelteKit and NextJS, I realized I have not been using Angular Universal Properly.

We know we need Server Side Rendering for SEO, but we forget how much quicker data can be loaded on the server.

How other frameworks handle this:

  • SvelteKit uses the load function with fetch, which transfers fetch data from server to client.
  • NextJS uses the getServerSideProps function, which only loads the data on the server.
  • Nuxt uses the asyncData function to load the data on the server.

All of these functions get the data asynchronously on the server before loading the page, and pass the data through cookies to the client. THIS SAVES YOU A READ TO YOUR API ENDPOINT!

Note: If your website does business in Europe, be sure to follow the GDPR cookies rule.

  • If your website loads your data, then goes blank, then everything is visible, you have a code problem.
  • If you have a loading spinner on data that can be loaded on the server, you're doing it wrong.

Server Function

Angular Universal does not have a server function. In fact, you can load server code anywhere. You just have to use check for Server or Browser:

import { isPlatformBrowser, isPlatformServer } from '@angular/common';
import { PLATFORM_ID } from '@angular/core';
...
  constructor(
    @Inject(PLATFORM_ID) platformId: Object
  ) {
    this.isBrowser = isPlatformBrowser(platformId);
    this.isServer = isPlatformServer(platformId);
Enter fullscreen mode Exit fullscreen mode

And check for it like so:

if (this.isBrowser) {
...
} else {
// server
...
}
...
// OR
...
if (this.isServer) {
...
} else {
// browser
...
}
Enter fullscreen mode Exit fullscreen mode

I personally put this in a service so that it is available everywhere. You could also technically go more platform specific with Angular Material, but not necessary.

WARNING: Unlike server only functions, your code within your conditional statements are still available in the client JS code.

Everybody already knows that... so what!?

What is not as well known, is how to transfer state from the server to the client. Luckily, Angular Universal has some state functions built in.

First, add BrowserTransferStateModule to app.module.ts and ServerTransferStateModule to app.server.module.ts.

import { 
  makeStateKey, 
  TransferState 
} from '@angular/platform-browser';

...

constructor(private transferState: TransferState) { ... }

saveState<T>(key: string, data: any): void {
  this.transferState.set<T>(makeStateKey(key), data);
}

getState<T>(key: string, defaultValue: any = []): T {
  const state = this.transferState.get<T>(
    makeStateKey(key),
    defaultValue
  );
  this.transferState.remove(makeStateKey(key));
  return state;
}

hasState<T>(key: string) {
  return this.transferState.hasKey<T>(makeStateKey(key));
}
Enter fullscreen mode Exit fullscreen mode

As you can see you can also remove the state or check for the existence of a state. You name it whatever you want with key.

Assuming this.post is displayed in your template, and getPost() fetches your post, you use it like so in your async function:

// firebase example
async getPost(): Observable<Post> {
  return docData<Post>(
    doc(this.afs, 'posts', id)
  );
}

// get data on server, save state
if (this.isServer) {
  const postData = await firstValueFrom(getPost());
  this.saveState('posts', postData);
  this.post = postData;
} else {

// retrieve state on browser
  this.post = this.hasState('posts')
    ? this.getState('posts')
    : await firstValueFrom(getPost());
}
Enter fullscreen mode Exit fullscreen mode

Because the data will already be in the DOM, Angular Universal uses change detection to automatically update the DOM. However, in this case, the DOM won't change.

Subscriptions

If you want to add subscriptions to your data, you will have two calls. The first call is on the server, the data is repopulated from the cookie on the browser, and then you sync the data with an observable on the front end.

postSub!: Subscription;


// retrieve state on browser
  this.post = this.hasState('posts')
    ? this.getState('posts')
    : await getPost();
  this.postSub = getPost()
    .subscribe((p: Post) => this.post = p);
}

...

ngOnDestroy(): void {
  if (this.postSub) this.postSub.unsubscribe();
}
Enter fullscreen mode Exit fullscreen mode

If you use the async pipe with subscriptions instead of subscribing in the component, you will need to use a behavioral subject that saves the firebase promise, and updates the DOM when the observable is ready.

Conclusion and Advice

  • You need to use cookies to pass data from the server to the browser, but all frameworks have built in functions.
  • You should not have a blank page (even for a second) when the data is loaded from the server.
  • See my Fireblog.io for how it should look with subscriptions. Here is Github.
  • Make sure your data is waiting properly in Angular with ZoneJS or you may not have any working example.

Until next time...

J

UPDATE: 2/15/22 - Correction. Angular Universal does not actually use cookies under the hood. See my latest post.

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