Skip to main content

Angular Transfer State

Introduction

When building an Angular application with Server-Side Rendering (SSR), one common challenge is avoiding duplicate data fetching. Without proper optimization, your application might fetch the same data twice - once on the server and then again on the client after the application bootstraps. This unnecessary duplication leads to wasted resources and potential flickering in the user interface.

Angular's Transfer State API provides an elegant solution to this problem. It allows you to "transfer" data fetched during server-side rendering to the client, eliminating the need to fetch the same data again when the application runs in the browser.

Understanding the Problem

Let's consider a typical scenario:

  1. User requests a page
  2. Server fetches data from an API, renders the HTML
  3. HTML is sent to the browser
  4. Angular bootstraps in the browser
  5. The same API calls are made again to get the same data

This duplication happens because the client-side Angular application has no knowledge of what data was already fetched on the server. Transfer State solves this by creating a mechanism to pass this information between server and client.

Getting Started with Transfer State

Step 1: Import Required Modules

First, you need to include the necessary modules in your application. In your app.module.ts:

typescript
import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser';
import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';

// For client-side (browser) module
@NgModule({
imports: [
BrowserModule,
BrowserTransferStateModule,
// other imports
],
// declarations, providers, etc.
})
export class AppModule { }

// For server-side module
@NgModule({
imports: [
AppModule,
ServerModule,
ServerTransferStateModule,
// other server-side imports
],
// declarations, providers, etc.
})
export class AppServerModule { }

Step 2: Using TransferState in Your Components

The TransferState service can be injected into components or services where you want to manage state:

typescript
import { Component, OnInit } from '@angular/core';
import { TransferState, makeStateKey } from '@angular/platform-browser';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';

// Define a key for your data
const USERS_KEY = makeStateKey<any[]>('users');

@Component({
selector: 'app-users',
template: `
<div *ngIf="users$ | async as users">
<h2>User List</h2>
<ul>
<li *ngFor="let user of users">{{ user.name }}</li>
</ul>
</div>
`
})
export class UsersComponent implements OnInit {
users$: Observable<any[]>;

constructor(
private http: HttpClient,
private transferState: TransferState
) {}

ngOnInit() {
// Check if the data exists in the transfer state
if (this.transferState.hasKey(USERS_KEY)) {
// If it exists, use it
const users = this.transferState.get(USERS_KEY, []);
this.users$ = of(users);

// Remove it from transfer state (optional)
this.transferState.remove(USERS_KEY);
} else {
// If it doesn't exist, fetch it and store it
this.users$ = this.http.get<any[]>('https://api.example.com/users')
.pipe(
tap(users => {
if (typeof window === 'undefined') {
// We're on the server, so store the result
this.transferState.set(USERS_KEY, users);
}
})
);
}
}
}

Advanced Usage: Creating a Reusable Service

For more modularity and cleaner components, you can create a service to handle transfer state:

typescript
import { Injectable } from '@angular/core';
import { TransferState, makeStateKey } from '@angular/platform-browser';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable({
providedIn: 'root'
})
export class DataTransferService {
constructor(
private http: HttpClient,
private transferState: TransferState
) {}

getWithTransfer<T>(key: string, url: string): Observable<T> {
const stateKey = makeStateKey<T>(key);

if (this.transferState.hasKey(stateKey)) {
const data = this.transferState.get(stateKey, null as T);
this.transferState.remove(stateKey);
return of(data);
} else {
return this.http.get<T>(url).pipe(
tap(data => {
if (typeof window === 'undefined') {
this.transferState.set(stateKey, data);
}
})
);
}
}
}

Usage in component:

typescript
@Component({
selector: 'app-products',
template: `
<div *ngIf="products$ | async as products">
<div *ngFor="let product of products" class="product-card">
<h3>{{ product.name }}</h3>
<p>{{ product.description }}</p>
<span>{{ product.price | currency }}</span>
</div>
</div>
`
})
export class ProductsComponent implements OnInit {
products$: Observable<any[]>;

constructor(private dataTransfer: DataTransferService) {}

ngOnInit() {
this.products$ = this.dataTransfer.getWithTransfer<any[]>(
'products',
'https://api.example.com/products'
);
}
}

Real-World Example: Blog with Comments

Let's look at a more comprehensive example of a blog post page with comments, using Transfer State to avoid duplicate fetching:

typescript
// blog-post.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { TransferState, makeStateKey } from '@angular/platform-browser';
import { Observable, of, forkJoin } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable({
providedIn: 'root'
})
export class BlogPostService {
private apiUrl = 'https://api.example.com';

constructor(
private http: HttpClient,
private transferState: TransferState
) {}

getPost(id: string): Observable<any> {
const postKey = makeStateKey<any>(`post-${id}`);
const commentsKey = makeStateKey<any[]>(`post-comments-${id}`);

if (this.transferState.hasKey(postKey) && this.transferState.hasKey(commentsKey)) {
// Get data from transfer state
const post = this.transferState.get(postKey, null);
const comments = this.transferState.get(commentsKey, []);

// Clean up transfer state (optional)
this.transferState.remove(postKey);
this.transferState.remove(commentsKey);

return of({
post,
comments
});
} else {
// Fetch both post and comments in parallel
const post$ = this.http.get<any>(`${this.apiUrl}/posts/${id}`);
const comments$ = this.http.get<any[]>(`${this.apiUrl}/posts/${id}/comments`);

return forkJoin({
post: post$,
comments: comments$
}).pipe(
tap(data => {
if (typeof window === 'undefined') {
// Store in transfer state on server
this.transferState.set(postKey, data.post);
this.transferState.set(commentsKey, data.comments);
}
})
);
}
}
}

In the blog post component:

typescript
// blog-post.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { BlogPostService } from './blog-post.service';

@Component({
selector: 'app-blog-post',
template: `
<ng-container *ngIf="blogData$ | async as data">
<article class="blog-post">
<h1>{{ data.post.title }}</h1>
<div class="meta">
<span>By {{ data.post.author }}</span>
<span>{{ data.post.publishDate | date }}</span>
</div>
<div class="content" [innerHTML]="data.post.content"></div>
</article>

<section class="comments">
<h2>Comments ({{ data.comments.length }})</h2>
<div *ngFor="let comment of data.comments" class="comment">
<h4>{{ comment.user }}</h4>
<p>{{ comment.text }}</p>
<small>{{ comment.date | date }}</small>
</div>
</section>
</ng-container>
`
})
export class BlogPostComponent implements OnInit {
blogData$: Observable<{post: any, comments: any[]}>;

constructor(
private route: ActivatedRoute,
private blogService: BlogPostService
) {}

ngOnInit() {
this.blogData$ = this.route.paramMap.pipe(
switchMap(params => {
const id = params.get('id');
return this.blogService.getPost(id);
})
);
}
}

Transfer State with Lazy Loaded Modules

When working with lazy-loaded modules, you'll need to ensure Transfer State is properly set up:

  1. Make sure BrowserTransferStateModule is imported in your root AppModule
  2. Import ServerTransferStateModule in your AppServerModule
  3. You don't need to import these modules in your lazy-loaded modules

Best Practices and Tips

1. Use Descriptive Keys

Choose meaningful, unique keys for your transfer state to avoid conflicts:

typescript
// Good
makeStateKey<User[]>('user-list-page-1');

// Avoid
makeStateKey<any>('data');

2. Clean Up After Use

To prevent memory leaks and ensure clean state, remove data from the transfer state after consuming it:

typescript
const usersKey = makeStateKey<User[]>('users');
const users = this.transferState.get(usersKey, []);
this.transferState.remove(usersKey); // Clean up

3. Type Your Data

Always specify the type of your transfer state data for better type safety:

typescript
interface Product {
id: number;
name: string;
price: number;
}

const productsKey = makeStateKey<Product[]>('products');

4. Conditionally Set State on Server Only

Only set transfer state when on the server to avoid unnecessary operations:

typescript
if (isPlatformServer(this.platformId)) {
this.transferState.set(dataKey, result);
}

Common Pitfalls and Solutions

Pitfall 1: State Not Transferring

If your state isn't being transferred, check:

  • Ensure both BrowserTransferStateModule and ServerTransferStateModule are correctly imported
  • Verify you're checking for state before making HTTP requests
  • Confirm the key names match exactly between server and client

Pitfall 2: Circular JSON Structures

Transfer State serializes data as JSON, so circular structures will cause errors:

typescript
// Will cause errors
const circular = { self: null };
circular.self = circular;
this.transferState.set(makeStateKey('circular'), circular);

Solution: Ensure your data is serializable by removing circular references or using a library like flatted to handle circular JSON.

Pitfall 3: Too Much Data

Transferring large datasets can increase initial page load size. Consider:

  • Only transfer critical data needed for initial render
  • Paginate or lazy-load less important data
  • Compress large data structures

Summary

Angular's Transfer State API is a powerful tool for optimizing SSR applications by eliminating duplicate data fetching between server and client. By implementing this pattern, you can:

  • Improve performance by avoiding unnecessary network requests
  • Eliminate UI flickering caused by re-fetching data
  • Create a smoother user experience with faster perceived load times

The basic workflow is:

  1. Check if data exists in transfer state
  2. If it does, use it directly
  3. If not, fetch the data and store it in transfer state (when on server)

By following the patterns and examples in this guide, you can effectively implement Transfer State in your Angular SSR applications and deliver a better user experience.

Additional Resources

Exercises

  1. Create a simple news application that fetches and displays articles using Transfer State
  2. Modify an existing application to use Transfer State for API calls
  3. Build a service that handles both Transfer State and client-side caching
  4. Implement Transfer State with GraphQL queries using Apollo Client
  5. Create a performance benchmark comparing an application with and without Transfer State


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)