Using TransferState API in an Angular v5 Universal App

Philippe Martin
Angular In Depth
Published in
4 min readOct 8, 2017

AngularInDepth is moving away from Medium. More recent articles are hosted on the new platform inDepth.dev. Thanks for being part of indepth movement!

You can get a more up-to-date version at https://leanpub.com/angular-universal

Let’s illustrate this article with a concrete example. We have a weather app, displaying a list of cities in its sidebar. When you click on a city name, the app displays the current weather in this city.

Because we want our app to be crawlable and indexable, we make it universal: city pages are rendered on the server, stored as HTML files and served by an HTTP server. These pages will contain the browser app, so the user can continue to navigate in the app by using the power of Angular once he has loaded the first page.

You can try this simple example by following these steps.

A typical Universal app

First clone the following repo and checkout the initial tag:

$ git clone https://github.com/feloy/ng-demo-transfer-state
$ cd ng-demo-transfer-state
$ git checkout initial

Then build the browser ans server apps:

$ npm install
$ ng build -prod
$ ng build -prod -app server --output-hashing=none

Finally create the pages for the different cities:

$ node render-page.js /Paris > dist/Paris
$ node render-page.js /London > dist/London
$ node render-page.js /San%20Fransisco > 'dist/San Fransisco'

You can now serve the dist directory with your preferred HTTP server (you will probably have to configure you server to serve files without extensions with a text/html content type).

Now, if you access the page http://your-domain/Paris directly (this the typical case when visitors are coming from a search engine), you can observe that the page flashes — this is because the content, which is already present in the HTML downloaded and so already displayed, is reloaded by the browser app and displayed again.

TransferState to the rescue

The TransferState API introduced in Angular v5 can help in this situation. It can transfer data from the server side of the app to the browser app.

For this, the server app will add the data we want to transfer in the HTML page it generates.

The browser app, contained in this generated HTML page, will then be able to read this data.

Let’s proceed. The API documentation can be found here: https://next.angular.io/api/platform-browser/TransferState

You can get the resulting sources by checking out the transfer-data tag of the repo:

$ git checkout transfer-data

First import ServerTransferStateModule on the server app and BrowserTransferStateModule on the browser app:

// src/app/app.server.module.tsimport {ServerTransferStateModule} from '@angular/platform-server';
[...]
@NgModule({
imports: [
AppModule,
ServerModule,
ServerTransferStateModule
],
bootstrap: [AppComponent],
})
export class AppServerModule {}
// src/app/app.module.tsimport { BrowserTransferStateModule } from '@angular/platform-browser';
[...]
@NgModule({
declarations: [
AppComponent, CityComponent
],
imports: [
BrowserModule.withServerTransition({ appId: 'ng-demo-transfer-state-app' }),
BrowserTransferStateModule,
[...]

Now, in the resolver that provides the data for the component, we can use the TransferState API:

  • on the server, we first register to onSerialize to provide the data we will download, then we get data from our data provider, here with an HTTP GET request.
  • on the browser, we use the get method to retrieve the data provided by the serve, and we directly provide this data. We also remove the provided data from the transferred state, so the reload of the page will not use the provided data anymore.

We can detect if we are on the server or on the browser app by calling the hasKey method. This method will return true only in the browser.

resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<CityWeather> {
const found = this.transferState.hasKey(RESULT_KEY);
if (found) {
const res = Observable.of(this.transferState.get<CityWeather>(RESULT_KEY, null));
this.transferState.remove(RESULT_KEY);
return res;
} else {
this.transferState.onSerialize(RESULT_KEY, () => this.result);
const name = route.params['city'];
return this.http.get<CityWeather>('https://api.openweathermap.org/data/2.5/weather?q=' + name + '&units=metric&APPID=' + this.key)
.do(result => this.result = result);
}
}

Because we are calling the remove method to remove the provided data, the following pages displayed on the browser will call onSerialize method, but this method has no effect because the toJson is called only on the server.

A clearer solution would be to use the isPlatformServer and isPlatformBrowser methods to detect the platform and act accordingly.

--

--