Transfer State with Angular

Updated : 08/09/2023 danny

The httpclient module associated with the Side Rendering Server ( SSR) generates two requests during an API call.

Which doubles the workload on a backend.

The objective of this tutorial is therefore to improve the SSR of our Angular application.

We are going to add two modules ServerTransferStateModule and BrowserTransferStateModule to our project.

We will use the Angular ​​javascript framework.

Transfer state avec Angular

What are we going to do ?

Important note.

This functionality is no longer useful on Angular 15, the Google team having resolved the malfunction.
This tutorial is therefore only useful with Angular 14 or earlier versions.
​​​​​​​

This is the step of our Angular guide which will allow us to obtain a PWA type Web Application .

The base Angular project we will use already has the following features

  • Generated with Angular CLI
  • Routing
  • Lazy Loading
  • CSS Bootstrap Framework
  • Server Side Rendering
  • HttpClient

All sources created are indicated at the end of the tutorial.

The application is at the following address


Verification

An API call in our code initially launches two requests to the API server.
We will first check the theory.

The code we are going to analyze is in the items.component.ts file
For this we will make some changes in this file .

src/app/modules/application/items/items.component.ts
import { Component, OnInit } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { PLATFORM_ID, APP_ID, Inject } from '@angular/core';

import { ItemsService } from './items.service';

@Component({
  selector: 'app-items',
  templateUrl: './items.component.html',
  styleUrls: ['./items.component.css']
})
export class ItemsComponent implements OnInit {

  items: any;
  loaded: boolean;
  constructor(
    private itemsService: ItemsService,
    @Inject(PLATFORM_ID) private platformId: Object,
    @Inject(APP_ID) private appId: string) {
      this.loaded = false;
  }


  ngOnInit(): void {
    this.getUsers();
  }

  getUsers(): void {
    this.itemsService.getItems('https://jsonplaceholder.typicode.com/users')
      .subscribe(
        items => {
          const platform = isPlatformBrowser(this.platformId) ?
            'in the browser' : 'on the server';
          console.log(`getUsers : Running ${platform} with appId=${this.appId}`);
          this.loaded = true;
          this.items = items;
        });
  }

  resetUsers(): void {
    this.items = null;
    this.loaded = true;
  }

}

We will now verify this theory.

Case no. 1 (without SSR)

  • npm run start
  • Launch Chrome
  • Enable Developer Tools with Ctrl+Shift+J
  • Launch http://localhost:4200/httpclient
  • in chrome console check a query call in browser
  • getUsers: Running in the browser with appId=angular-starter


Case 2 (with SSR)

  • npm run build:ssr
  • npm run serve:ssr
  • Launch Chromium
  • Enable Developer Tools with Ctrl+Shift+J
  • Launch http://localhost:4000/httpclient
  • in chrome console check a query call in browser
  • getUsers: Running in the browser with appId=angular-starter
  • In the console launching the prompt check a query call in the server
  • Node server listening on http://localhost:4000
  • getUsers: Running on the server with appId=angular-starter

Modification

The solution is to use two Angular modules.
ServerTransferStateModule and BrowserTransferStateModule .

To do this we need to modify some of our files.
The steps are as follows.

  • Edit main.ts
  • Create src/app/ app.browser.module.ts
  • Edit app.server.module.ts
  • Modify app.module.ts (adding CommonModule)
  • Edit items.component.ts
  • Edit items.component.spec.ts
  • Edit tslint.json
src/main.ts
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppBrowserModule } from './app/app.browser.module';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

function bootstrap() {
     platformBrowserDynamic().bootstrapModule(AppBrowserModule)
  .catch(err => console.error(err));
   };


if (document.readyState === 'complete') {
  bootstrap();
} else {
  document.addEventListener('DOMContentLoaded', bootstrap);
}

src/app/app.browser.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser';

import { AppModule } from './app.module';
import { AppComponent } from './app.component';

@NgModule({
  imports: [
    AppModule,
    BrowserModule.withServerTransition({ appId: 'angular-starter' }),
    BrowserTransferStateModule
  ],
  bootstrap: [AppComponent],
})
export class AppBrowserModule { }
src/app/app.server.module.ts
import { NgModule } from '@angular/core';
import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';
import { BrowserModule } from '@angular/platform-browser';

import { AppModule } from './app.module';
import { AppComponent } from './app.component';

@NgModule({
  imports: [
    AppModule,
    ServerModule,
    ServerTransferStateModule,
    BrowserModule.withServerTransition({ appId: 'angular-starter' }),
  ],
  bootstrap: [AppComponent],
})
export class AppServerModule { }
src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { HomeComponent } from './modules/general/home/home.component';
import { NotFoundComponent } from './modules/general/not-found/not-found.component';
import { AppRoutingModule } from './app-routing.module';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    NotFoundComponent,
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
src/app/modules/items/items.component.ts
import { Component, OnInit } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { TransferState, makeStateKey } from '@angular/platform-browser';
import { PLATFORM_ID, APP_ID, Inject } from '@angular/core';

import { ItemsService } from './items.service';

const STATE_KEY_ITEMS = makeStateKey('items');

@Component({
  selector: 'app-items',
  templateUrl: './items.component.html',
  styleUrls: ['./items.component.css']
})
export class ItemsComponent implements OnInit {

  //  items: any;
  items: any = [];
  loaded: boolean;
  constructor(
    private state: TransferState,
    private itemsService: ItemsService,
    @Inject(PLATFORM_ID) private platformId: object,
    @Inject(APP_ID) private appId: string) {
      this.loaded = false;
  }

  ngOnInit(): void {
    this.getUsers();
  }

  getUsers(): void {
    this.loaded = false;

    this.items = this.state.get(STATE_KEY_ITEMS, <any> []);

    if (this.items.length === 0) {
      this.itemsService.getItems('https://jsonplaceholder.typicode.com/users')
        .subscribe(
          items => {
            const platform = isPlatformBrowser(this.platformId) ?
              'in the browser' : 'on the server';
            console.log(`getUsers : Running ${platform} with appId=${this.appId}`);
            this.items = items;
            this.loaded = true;
            this.state.set(STATE_KEY_ITEMS, <any> items);
          });
    } else {
      this.loaded = true;
    }
  }

  resetUsers(): void {
    this.items = null;
    this.loaded = true;
  }

}
src/app/modules/items/items.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClientModule } from '@angular/common/http';
import { ItemsComponent } from './items.component';
import { BrowserTransferStateModule } from '@angular/platform-browser';

describe('ItemsComponent', () => {
  let component: ItemsComponent;
  let fixture: ComponentFixture<ItemsComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [
        HttpClientModule,
        BrowserTransferStateModule        
      ],
      declarations: [ItemsComponent]
    }).compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(ItemsComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

Conclusion

Perform the previous test to see that there is only one API call on the server.

  • npm run build:ssr
  • npm run server:ssr
  • localhost:4000/httpclient


All that remains is to test the different Angular scripts to finalize the application.

# Développement
npm run start
http://localhost:4200/

# Tests
npm run lint
npm run test
npm run e2e

# AOT compilation
npm run build

# SSR compilation
npm run build:ssr
npm run serve:ssr
http://localhost:4000/