How to Properly Fetch Data in Angular Universal
Tags: angular, angularuniversal, firestore, firebase
Published: 2022-02-12 20:57:07

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](https://kit.svelte.dev/docs/migrating#pages-and-layouts-preload) uses the `load` function with `fetch`, which transfers fetch data from server to client.
- [NextJS](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props) uses the `getServerSideProps` function, which only loads the data on the server.
- [Nuxt](https://nuxtjs.org/docs/features/data-fetching/#async-data) 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](https://gdpr.eu/cookies/) 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:

```typescript
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);
```

And check for it like so:

```typescript
if (this.isBrowser) {
...
} else {
// server
...
}
...
// OR
...
if (this.isServer) {
...
} else {
// browser
...
}
```

I personally put this in a [service](https://github.com/jdgamble555/fireblog.io/blob/master/src/app/nav/nav.service.ts) so that it is available everywhere. You could also technically go more [platform specific](https://material.angular.io/cdk/platform/overview) 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`.

```typescript
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));
}
```
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:

```typescript
// 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());
}
```
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.

```typescript
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();
}
```
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](https://fireblog.io) for how it should look with subscriptions. Here is [Github](https://github.com/jdgamble555/fireblog.io).
- Make sure your data is [waiting properly](https://dev.to/jdgamble555/forcing-angular-to-wait-on-your-async-function-2ck1) 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](https://dev.to/jdgamble555/passing-state-from-the-server-to-the-browser-in-sveltekit-37co).