DEV Community

Deekshith Raj Basa πŸ”₯
Deekshith Raj Basa πŸ”₯

Posted on

Angular Server-Side Rendering(SSR): The Browser Is Not The Server

Alt Text

One of the great things about SSR is that we get to use the same code on our frontend and our backend to render our app. Well, sort of.

When we use the same code, right off the bat we have a problem: the browser is not the server and there are differences between what we can do in each environment.

The benefit of rendering our Angular app on the server is that we can fetch data privately and efficiently before we send anything to the browser.

Our server is (in this case) Node.js, and so on the server we can use:

  • request to make HTTP requests retrieved by the server (and these can be private authenticated requests)
  • fs to access the filesystem (if we need to) access to anything else you'd want on the server: Redis, AWS services, the database etc.

But the browser is not the server. And if we try to call browser-only APIs, then we'll break SSR.

What can break SSR?

Well, three things come to mind that is exclusive to the browser:

  • the window can be used to e.g. display alerts to the user
  • the document belongs to the window namespace and is used to manipulate DOM elements
  • navigator belongs to the window namespace and enables service workers that are used extensively with Progressive Web Applications

While it's awesome that our Angular application can share code between the server and the browser, if we want to use any of these objects, we need to execute a different logic path based on the current runtime: Node.js or the browser window.

Below, I'm going to show you one of the techniques for doing that

Adding internationalization

Let's add internationalization to your application. Let's display product prices in three currencies: US dollars, British pounds, and Polish zloty. The application should pick a currency based on browser settings, and if a given language is not supported, it should fall back to Polish zloty

Let's generate a new service:

ng g s sample

Now let's detect user language and implement the getCurrencyCode() method that returns one of the three available currency codes:

  providedIn: 'root'
})
export class SampleService {

  private userLang;

  constructor() { 
      this.userLang = window.navigator.language;
  }

  public getCurrencyCode(): string {
    switch(this.userLang) {
      default: 
      case 'pl-PL': return 'PLN';
      case 'en-US': return 'USD';
      case 'en-EN': return 'GBP';
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now in one of our components, say ProductDetailsComponent, we can use this service to get the user's currency:

public userCurrency: string = this.sampleService.getCurrencyCode();

constructor(
  private route: ActivatedRoute, 
  private ps: ProductsService, 
  private us: UserService, 
  private sampleService: SampleService
) { }
Enter fullscreen mode Exit fullscreen mode

Then we could use the userCurrency in a view with the currency pipe:

<pclass="text-muted">{{userCurrency}}</p>

From now on, prices should display in a currency defined by the user's localization settings. This is great, right?

Well, no. Unfortunately, this logic breaks SSR:

ERROR: ReferenceError: window is not defined

It would help if we had a mechanism to detect whether the current runtime is the browser or the server - and thankfully that's why we have isPlatformBrowser() and isPlatformServer():

isPlatformBrowser() and isPlatformServer()

Angular ships with the isPlatformBrowser() and isPlatformServer() methods in the @angular/common package. Each of these methods accepts one parameter: the platform ID. It can be retrieved via the Dependency Injection mechanism using the injection token PLATFORM_ID available in the @angular/core package.

So to change our internationalization service I18nService above, Add these new imports:

import { 
  Injectable, 
  Inject, 
  PLATFORM_ID 
  } from '@angular/core';
import { 
  isPlatformBrowser 
  } from '@angular/common';
Enter fullscreen mode Exit fullscreen mode

Modify the service constructor to only use the window object if an instance of the service executes in the browser:

export class SampleService {
  constructor(
    @Inject(PLATFORM_ID)
    private platformId: any
  ) {
    if (isPlatformBrowser(this.platformId)) {
      this.userLang =
        window.navigator.language;
    } else {
      // server specific logic
    }
  }
  // ...
} 
Enter fullscreen mode Exit fullscreen mode

This should be enough for SSR to start working again but we don't get internationalization pre-rendered on our server-side render -- internationalization won't appear until after the app loads.

So what we need is a way to know what language to render from the origin HTTP request to the server.

The Request object
The question now is how to retrieve information about user language on the server. Is it even possible?

Yes, it is.

When you're performing a request from the browser, the browser adds a bunch of HTTP headers that you might not usually think about.

One of these headers is Accept-Language which tells us what language the user wants!

For example, the header might come through like this: Accept-Language: en-US, en;q=0.5

Getting Headers from the Request

Angular Universal allows you to get an object that represents an HTTP request. It's available via Dependency Injection under the REQUEST token from the @nguniversal/express-engine/tokens package. The Request object contains the following fields:

  • body
  • params
  • headers
  • cookies

So we update our imports by adding the Request object, the REQUEST injection token, and the Optional decorator

import { Injectable, Inject, PLATFORM_ID, Optional } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { REQUEST } from '@nguniversal/express-engine/tokens';
import { Request } from 'express';
Enter fullscreen mode Exit fullscreen mode

Change the constructor to inject the Request object and retrieve user language from the Accept-Language header:

export class SampleService {
  constructor(
    @Inject(PLATFORM_ID) private platformId: any,
    @Optional()
    @Inject(REQUEST) private request: Request
  ) {
    if (isPlatformBrowser(this.platformId)) {
      this.userLang =
        window.navigator.language;
    } else {
      this.userLang = (
        this.request.headers[
          "accept-language"
        ] || ""
      ).substring(0, 5);
    }
  }
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Discussion (3)

Collapse
andlewis profile image
Andrew Lewis

Angular Universal is a great proof of concept, but I’ve never seen an officially supported implementation, or one that actually uses the current version of angular, or even decent documentation. It seems like someone thought it would be a neat feature to add a while ago and got it 80% there, then never touched it again.

Collapse
navix profile image
Oleksa Novyk

I use SSR in production a lot. It works fine.

You can check out the official guide: angular.io/guide/universal

With Angular 9 also added pre-rendering feature, it is a simple way to render your app on build (of course if you know all the routes). Nice guide: samvloeberghs.be/posts/angular-v9-...

Collapse
deekshithrajbasa profile image
Deekshith Raj Basa πŸ”₯ Author

Wow that an in detail explaination with an example