When you’re rendering your Angular Universal application on the server, it will actually make the
GET requests which your application triggers on load, on the server.
This causes your
GET requests to be triggered twice. Once on the server and once on the client side.
Making the request on the client side will delay the visibility of the content (because the data is just not there), and most search engines doesn’t like that at all.
Now this is a problem, so how can we fix it?
There’s probably a couple solutions out there, but the simplest solution in my opinion would be to introduce an interceptor.
As this might not be the best solution for complex applications, it can definitely work well in most cases.
Let’s have a look at the required steps to introduce a simple server-to-client state transfer in your Angular Universal application:
Add BrowserTransferStateModule to the imports array of app.module.ts
Add ServerTransferStateModule to the imports array of app.server.module.ts
Create a browser-state.interceptor.ts and provide it to app.module.ts
Create a server-state.interceptor.ts and provide it to app.server.module.ts
Including BrowserTransferStateModule in app.module.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import { NgModule } from '@angular/core'; import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule.withServerTransition({ appId: 'dank-app' }), BrowserTransferStateModule, ], bootstrap: [ AppComponent ] }) export class AppModule { } |
Including ServerTransferStateModule in app.server.module.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import { NgModule } from '@angular/core'; import { ServerModule, ServerTransferStateModule } from '@angular/platform-server'; import { AppModule } from './app.module'; import { AppComponent } from './app.component'; @NgModule({ imports: [ AppModule, ServerModule, ServerTransferStateModule, ], bootstrap: [ AppComponent ], }) export class AppServerModule { } |
Creating browser-state.interceptor.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import { Injectable } from '@angular/core'; import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse } from '@angular/common/http'; import { TransferState, makeStateKey } from '@angular/platform-browser'; import { Observable } from 'rxjs/Observable'; import { of } from 'rxjs/observable/of'; @Injectable() export class BrowserStateInterceptor implements HttpInterceptor { constructor( private _transferState: TransferState, ) { } intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { if (req.method !== 'GET') { return next.handle(req); } const storedResponse: string = this._transferState.get(makeStateKey(req.url), null); if (storedResponse) { const response = new HttpResponse({ body: cachedResponse, status: 200 }); return of(response); } return next.handle(req); } } |
Implement the HttpInterceptor interface to get some strong typing. As it is optional to implement lifecycle hooks on your classes, it’s quite useful for in-editor debugging.
intercept(...) is triggered on every HTTP request when you’re using the HttpClient module.
Because of the fact we only get data to store on
GET requests, we will make an if statement checking the type of request method:
req.method !== 'GET'.
If it’s not a
GET request we will simply let the
HttpHandler take care of business.
When it is indeed a GET request, then we want to look for data in the TransferState. If there’s something for us there, then we will return the result as an Observable , because this is what HttpClient normally would give us. That way we dont need to change anything in our Effects, Components, etc.
Use the requested URL as the identifying key: makeStateKey(req.url)
If for some reason there should not be anything for us in the TransferState , then we will let the HttpHandler take care of business once again.
We also have to provide this new interceptor in our app.module.ts.
Import our BrowserStateInterceptor and then provide it as follows:
1 2 3 4 5 6 7 8 9 |
... providers: [ { provide: HTTP_INTERCEPTORS, useClass: BrowserStateInterceptor, multi: true, }, ], ... |
Creating server-state.interceptor.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import { Injectable } from '@angular/core'; import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse } from '@angular/common/http'; import { TransferState, makeStateKey } from '@angular/platform-browser'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/do'; @Injectable() export class ServerStateInterceptor implements HttpInterceptor { constructor( private _transferState: TransferState, ) {} intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { return next.handle(req).do(event => { if (event instanceof HttpResponse) { this._transferState.set(makeStateKey(req.url), event.body); } }); } } |
The serverside interceptor does share alot of the same concepts, but obviously we want it do operate a little different.
This interceptor should always store data for the client side, if the event is an instance of
HttpResponse.
Because we used the requested url as the identifying key on the client, it’s easy for us to have the same key generated on the serverside. Simply do the same!
And of course we also have to provide this new interceptor, but in the app.server.module.ts instead.
Import our ServerStateInterceptor and provide it as follows:
1 2 3 4 5 6 7 8 9 |
... providers: [ { provide: HTTP_INTERCEPTORS, useClass: ServerStateInterceptor, multi: true, }, ], ... |
Celebration 🎉
By now your application should not make any
GET request in your browser, on the initial load.
You can test this by running your local nodejs server and have that serve the Angular Universal application for you!
Sometimes fixing a rather complex issue is just that simple.
Have fun and good luck with the state transfering. See you next time!