Mihai Marinescu
Mihai Marinescu's Blog

Mihai Marinescu's Blog

Angular Universal: passing server data to client in APP_INITIALIZER

Angular Universal: passing server data to client in APP_INITIALIZER

Mihai Marinescu's photo
Mihai Marinescu
·Oct 9, 2021·

3 min read

If you keep data about the user in session of the express server that Angular Universal uses, you need to pass this data when the user first loads the page.

For a smooth data flow, you need to do the following:

  • Create an injection token for the data you want to pass. I'm using a separate file for the tokens;
 import {InjectionToken} from '@angular/core';

 export const USER_PROFILE = new InjectionToken<object>('user data');
  • In your server.ts file, provide this token with the data in the res.render call
// All regular routes use the Universal engine
app.get('*', (req, res) => {

res.render(indexHtml, {
  req,
  providers: [
    {provide: APP_BASE_HREF, useValue: req.baseUrl},
    {provide: USER_PROFILE, useValue: req.session.user}
  ]
 });
});
  • Make sure you have BrowserTransferStateModule in app.module.ts imports and ServerTransferStateModule in app.server.module.ts imports

  • Create a TransferState service for easier use of the transferstate keys creation and setting and getting values. I'm using code from this tutorial found here.

import {Inject, Injectable, PLATFORM_ID} from '@angular/core';
import {makeStateKey, StateKey, TransferState} from '@angular/platform-browser';
import {isPlatformServer} from '@angular/common';

@Injectable({
  providedIn: 'root'
})
export class TransferStateService {
  private keys = new Map<string, StateKey<string>>();

  constructor(
    @Inject(PLATFORM_ID) private readonly platformId: string,
    private readonly transferState: TransferState
  ) {}

  get<T>(
    key: string,
    defaultValue: T | null = null
  ): T | null {
    if (!this.has(key)) {
      return defaultValue || null;
    }

    return this.transferState.get<T>(
      this.getStateKey(key),
      <T>defaultValue
    );
  }

  has(key: string): boolean {
    return this.transferState.hasKey(this.getStateKey(key));
  }

  set(key: string, value: any): void {
    if (isPlatformServer(this.platformId)) {
      if (this.has(key)) {
        console.warn(
          `Setting existing value into TransferState using key: '${key}'`
        );
      }

      this.transferState.set(
       this.getStateKey(key),
       value
      );
    }
  }

  private getStateKey(key: string): StateKey<string> {
    if (this.keys.has(key)) {
      return <StateKey<string>> this.keys.get(key);
    }

    this.keys.set(key, makeStateKey(key));

    return <StateKey<string>> this.keys.get(key);
  }
}
  • Create an APP_INITIALIZER factory to use in app.module.ts providers
import {PLATFORM_ID, Injector} from '@angular/core';
import {isPlatformServer} from '@angular/common';
import {Store} from '@ngrx/store';

import {TransferStateService} from '@app/services/transfer-state.service';

export function StateTransferFactory(
  transferStateService: TransferStateService,
  injector: Injector,
  store: Store
): any {
  return () => {
    const platformId = injector.get(PLATFORM_ID);
    const isServer = isPlatformServer(platformId);
    let user: any;

    if (isServer) {
      user = injector.get(USER_PROFILE) || null;

      transferStateService.set('USER_PROFILE_KEY', user);

    } else {
      user = transferStateService.get('USER_PROFILE_KEY');
    }

    store.dispatch(setUser({user})) // I'm using ngrx store to save data in the app
 };
}
  • Attach this factory to the APP_INITIALIZER provider in app.module.ts NOTE: app.module.ts is imported in app.server.module.ts and runs both on the server and on the client
{
  provide: APP_INITIALIZER,
  useFactory: StateTransferFactory,
  deps: [TransferStateService, Injector, Store],
  multi: true
}

Notice the injector.get(USER_PROFILE), which is used to get the value from the injectionToken. You use it only on the server, for the user token, because there is where you provide the value. On the client it would be undefined.

This way, whenever the user refreshes the app, you already have the store initialized with the user, if the user is on your req.session on the server.

 
Share this