Picture of Brian Love wearing black against a dark wall in Portland, OR.

Brian Love

Angular Transfer State

Learn how to effectively manage Angular Transfer State with server and static rendering using Universal.

What?

Angular’s TransferState class enables server-side rendered (SSR) and prerendered (SSR) Angular applications to efficiently render in the browser using fetched data from the server. Let’s break that down, starting first with the potential problem.

The problems

If you are using an SSR or prerendered application strategy then the process is roughly this:

  1. Prerender or render application on the server
  2. The browser fetches the rendered HTML and CSS and displays the “static” application
  3. The browser fetches, parses, interprets and executes JavaScript
  4. The Angular application is bootstrapped, replacing the entire DOM tree with the new “running” application
  5. The application is initialized, often fetching data from a remote server or API
  6. The user interacts with the application

There are two problems in this scenario:

  1. DOM hydration is currently replacing the entire tree of nodes and re-painting the application
  2. The application is fetching the data it, in theory, already had and was displayed to the user due to the SSR or prerendered site strategy

The first issue is something that is not currently solved in Angular, and quite frankly, is a complex challenge. However, the second issue is currently solved in Angular (version 9 as of this writing) using TransferState.

What is TransferState?

Transfer state is a strategy where we:

  1. Fetch the data required to render the full “static” application using either the SSR or prerendering strategies
  2. Serialize the data, and send the data with the initial document (HTML) response
  3. Parse the serialized data at runtime when the application is initialized, avoiding a redundant fetch of the data

Technically, the data is serialized via JSON.stringify() and parsed via JSON.parse(). Generally, we don’t need to worry about that, however, as that is performed for us by the TransferState service in Angular.

Let’s quickly review the documentation:

The values in the store are serialized/deserialized using JSON.stringify/JSON.parse. So only boolean, number, string, null and non-class objects will be serialized and deserialzied in a non-lossy manner.

If your application requires transferring state from the server to the client that does not conform to these requirements then you will need to implement a strategy for converting the data, both to, and from, what I’m calling “compliant” data.

My specific use case is serializing objects from Firestore that contain DocumentReference classes. A document reference is a foreign key in Firestore, which is a specialized class that the Firebase SDK provides.

Goals

My goals for using transfer state are to:

  1. Utilize RxJS data streams to “intercept” fetched data using SSR or prerendering
  2. Implement a strategy for converting non-compliant data into compliant data for serialization, and also, in turn, converting serialized compliant data back to the original structure

The third goal is necessary for my use case since I need to convert Firestore’s DocumentReference classes into something that can be serialized by TransferState.

Getting Started

To get started with the TransferState service we need to:

  1. Import the ServerTransferStateModule in our server application module.
  2. Import the BrowserTransferStateModule in our browser application module.

When using server-side rendering you will often have a separate application entry point, most commonly in the form of a src/main.server.ts file. And, the browser entry point is most commonly the src/main.ts file generated by the Angular CLI. These files will then bootstrap the application via a NgModule class.

So, let’s first open src/app/app.server.module.ts and import the ServerTransferStateModule:

@NgModule({
  imports: [AppModule, ServerModule, ServerTransferStateModule],
  bootstrap: [AppComponent],
})
export class AppServerModule {}

Then, let’s import the BrowserTransferStateModule into the src/app/app.module.ts file:

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule.withServerTransition({ appId: 'serverApp' }),
    BrowserTransferStateModule,
    CoreModule,
    // code omitted
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

TransferStateService

To solve the first goal of intercepting fetched data in RxJS streams I created a TransferStateService that can be provided to Angular component classes that fetch data.

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

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

  fetch<T>(
    key: string,
    observableInput: Observable<T>,
    defaultValue?: T,
    converter?: TransferStateConverter<T>,
  ): Observable<T> {
    if (this.has(key)) {
      return of(this.get(key, defaultValue, converter)).pipe(
        tap(() => this.remove(key)),
      );
    }
    return observableInput.pipe(
      tap((value) => this.set(key, value, converter)),
    );
  }

  get<T>(
    key: string,
    defaultValue?: T | null,
    converter?: TransferStateConverter<T>,
  ): T | null {
    if (!this.has(key)) {
      return defaultValue || null;
    }
    const value = this.transferState.get<T>(
      this.getStateKey(key),
      defaultValue,
    );
    return converter ? converter.fromTransferState(value) : value;
  }

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

  remove(key: string): void {
    if (!this.has(key)) {
      return;
    }
    this.transferState.remove(this.getStateKey(key));
  }

  set<T>(key: string, value: T, converter?: TransferStateConverter<T>): void {
    if (isPlatformServer(this.platformId)) {
      if (this.has(key)) {
        console.warn(
          `Setting existing value into TransferState using key: '${key}'`,
        );
      }
      if (!environment.production) {
        console.log(`Storing TransferState for: '${key}'`);
      }
      this.transferState.set(
        this.getStateKey(key),
        converter ? converter.toTransferState(value) : value,
      );
    }
  }

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

Let’s quickly review:

Here is a sample implementation of using the TransferStateService in an Angular resolver:

export class RuleResolver implements Resolve<Rule> {
  constructor(
    private readonly rulesService: RulesService,
    private readonly transferStateService: TransferStateService,
  ) {}

  resolve(snapshot: ActivatedRouteSnapshot) {
    return this.transferStateService.fetch<Rule>(
      RULE,
      this.rulesService.getBySlug(snapshot.paramMap.get('slug')),
    );
  }
}

Above we are using the fetch method for the injected TransferStateService instance to store the values fetched in the RuleService.getBySlug() method.

TransferStateConverter

As I previously mentioned in my specific use case I have non-compliant values that need to be converted for serialization into the TransferState service. To solve the second goal I wanted to create a well-defined, and type-safe, implementation for converting data.

First, let’s define a new abstract TransferStateConverter class:

import { Serializable } from '@lkt-core/types';

export abstract class TransferStateConverter<T> {
  /**
   * Called by the TransferStateService to convert a serialized value to an object of type T.
   */
  abstract fromTransferState(data: Serializable<T>): T;

  /**
   * Called by the TransferStateService to convert data to a value that is serializable by TransferState.
   */
  abstract toTransferState(data: T): Serializable<T>;
}

Two required methods must be implemented by converters:

  1. The toTransferState method accepts the non-compliant data and returns the Serializable value.
  2. The fromTransferState method accepts the Serializable value and returns the non-compliant value.

The toTransferState method will be executed in the context of the server or prerendering. And, the fromTransferState method will be executed in the context of the browser on the client.

You will also need to define a new Serializable type:

export type Serializable<T> = boolean | number | string | null | object | T;

The Serializable type represents values that can be serialized to JSON for use with the TransferState service.

Finally, let’s look at a sample implementation. We’ll stick with serializing a Rule. In my application, this is the object that is returned from Firestore that contains the non-compliant DocumentReference class.

Here is what a portion of the model interface looks like:

export interface Rule {
  // code omitted
  related?: Array<DocumentReference>;
  tags?: Array<DocumentReference>;
}

Note that the related and tags properties are an array of DocumentReference classes that are provided by the Firebase SDK.

Let’s create a new RuleConverter class that can be delegated to for converting the values, both from, and to, Serializable data:

type SerializedRule = Rule & {
  related?: string[];
  tags?: string[];
};

@Injectable({
  providedIn: 'root',
})
export class RuleConverter extends TransferStateConverter<Rule> {
  constructor(
    private readonly rulesService: RulesService,
    private readonly tagsService: TagsService,
  ) {
    super();
  }

  fromTransferState(rule: SerializedRule): Rule {
    return {
      ...rule,
      related: rule.related
        ? rule.related.map((id: string) => this.rulesService.getRef(id))
        : [],
      tags: rule.tags
        ? rule.tags.map((id: string) => this.tagsService.getRef(id))
        : [],
    };
  }

  toTransferState(rule: Rule): Serializable<Rule> {
    return {
      ...rule,
      related: rule.related ? rule.related.map((ref) => ref.id) : [],
      tags: rule.tags ? rule.tags.map((ref) => ref.id) : [],
    };
  }
}

Let’s skip over some of my specific implementation details, and rather, let’s focus on the implementation of the TransferStateConverter abstract class:

We can now implement the converter into the resolver:

const RULE = 'rule';

export class RuleResolver implements Resolve<Rule> {
  constructor(
    private readonly ruleConverter: RulesConverter,
    private readonly rulesService: RulesService,
    private readonly transferStateService: TransferStateService,
  ) {}

  resolve(snapshot: ActivatedRouteSnapshot) {
    return this.transferStateService.fetch(
      RULE,
      this.rulesService.getBySlug(snapshot.paramMap.get('slug')),
      null,
      this.ruleConverter,
    );
  }
}

What I like about this delation pattern is that I provide the RuleConverter class instance to the TransferStateService to effectively convert values to, and from, serializable values.

Conclusion

I’ve achieved both of my stated goals by defining a TransferStateService as well as a pattern for defining classes that extend the abstract TransferStateConverter class.