Using TransferState API in an Angular v5 Universal App
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.