Transfer State with Angular 14

Updated : 04/07/22 danny

What are we going to do?

We will add in our project two modules ServerTransferStateModule and BrowserTransferStateModule.
We will use the Angular version 13.1.1 javascript framework.

This is Step 7 of our  Angular guide which will allow us to obtain a PWA Web Application.
We will use an existing project whose characteristics are

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

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

The application is at the following address


Practice

The httpclient module associated with the SSR (side rendering server) generates two requests during an API call.
Doubling the workload on a backend


The goal of this tutorial is to improve the server side rendering (SSR) of our Angular application.


Vérification

An API call in our code starts two requests on the API server.
We will first check the theory.

The code we are going to parse is in the file items.component.ts
For that we will make some modifications 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 1 (without SSR)

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


Case 2 (with SSR)

  • npm run build:ssr
  • npm run serve:ssr
  • Launch Chrome
  • Enable development tools with Ctrl + Maj + J
  • Lancer http://localhost:4000/items
  • in the chrome console check a query call in the browser
  • getUsers : Running in the browser with appId=angular-starter
  • In the launch console of 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 et BrowserTransferStateModule.

For 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
  • Edit app.module.ts(rajout de 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 find that there is only one API call on the server.

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



It only remains to test the different Angular scripts to finalize the application.


# Development
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/