Brian F Love
Learn from a Google Developer Expert focused on Angular, Web Technologies, and Node.js from Portland, OR.
Ad·ultimatecourses.com
Learn Angular the right way with Ultimate Courses

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:

  • First, we inject the platformId string and TransferState class instance. We'll use the platformId to check if we are executing the Angular application in the context of the server. If you're using a browser-based prerendering strategy then you'll need to adjust this to determine when the application is being executed in the context of prerendering.
  • Then, we've defined a fetch method that accepts a key value that uniquely identifies the data, an observableInput, and optionally a defaultValue and converter. We'll talk more about the converter strategy below.
  • The fetch method "intercepts" the next notification and stores the value using the TransferState service. The set method in the class handles this.
  • The get method returns the value from the TransferState service if it exists, otherwise, it returns the defaultValue or null. It also handles converting data from the "compliant" serialized value back to the original value.
  • The has method returns a boolean value indicating if the data exists in the TransferState class.
  • The remove method removes the data from the TransferState class. This is to ensure that subsequent next notifications in our RxJS observable streams on the client are not the stale data that was fetched on the server-side.
  • The set method stores the data into the TransferState service, as well as converting non-compliant data into serializable values.
  • Finally, the private getStateKey method returns the StateKey for the key string provided.

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:

  • First, we extend the TransferStateConverter class providing the generic type for the converter. In this case, this converter is for converting a Rule.
  • The constructor() function provides injected dependencies for my implementation.
  • The fromTransferState() method accepts the rule argument, which is a SerializedRule and returns a Rule. In my implementation, I have converted a document id, which has been converted from the DocumentReference to a string using the getRef() method in my service.
  • The toTransferState() method accepts the rule argument, which is the non-compliant Rule object that contains the DocumentReference classes. This method converts references to the id string values.

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.

Brian F Love

Hi, I'm Brian. I am interested in TypeScript, Angular and Node.js. I'm married to my best friend Bonnie, I live in Portland and I ski (a lot).