Skip to main content

TransferStateService

The TransferStateService allows you to transfer your Angular application’s state into the static site rendered by Scully. This enables Scully to use cached data instead of making requests to the original data source (typically an external API).

Overview

When Scully renders your application, it captures the state of your data and embeds it in the static HTML. When users visit the static site, this cached state is loaded instantly, avoiding unnecessary API calls and improving performance. Additionally, the service loads state for subsequent route changes, allowing the next route’s data to be fetched from the server without requiring a full page reload.

Usage

The service is automatically provided when you import ScullyLibModule:
import { ScullyLibModule } from '@scullyio/ng-lib';

@NgModule({
  imports: [
    ScullyLibModule.forRoot({
      useTransferState: true  // Enable TransferState (default: true)
    })
  ]
})
export class AppModule { }

Methods

useScullyTransferState()

The recommended method for automatically including your observable data sources in Scully’s TransferState.
useScullyTransferState<T>(name: string, originalState: Observable<T>): Observable<T>
Parameters:
  • name - Unique identifier for this state
  • originalState - Your original observable data source
Returns: An observable that returns cached data in production and original data during development Example:
import { Component, OnInit } from '@angular/core';
import { TransferStateService } from '@scullyio/ng-lib';
import { Observable } from 'rxjs';

interface BlogPost {
  id: number;
  title: string;
  content: string;
}

@Component({
  selector: 'app-blog-post',
  template: `
    <article *ngIf="post$ | async as post">
      <h1>{{ post.title }}</h1>
      <div>{{ post.content }}</div>
    </article>
  `
})
export class BlogPostComponent implements OnInit {
  post$: Observable<BlogPost>;

  constructor(
    private http: HttpClient,
    private transferState: TransferStateService,
    private route: ActivatedRoute
  ) {}

  ngOnInit() {
    const postId = this.route.snapshot.params['id'];
    const originalState$ = this.http.get<BlogPost>(`/api/posts/${postId}`);
    
    // Wrap with TransferState
    this.post$ = this.transferState.useScullyTransferState(
      `post-${postId}`,
      originalState$
    );
  }
}

getState()

Returns an observable that fires once and completes after the page’s navigation has finished.
getState<T>(name: string): Observable<T>
Parameters:
  • name - The key name of the state to retrieve
Returns: An observable that emits the cached state value Example:
import { Component, OnInit } from '@angular/core';
import { TransferStateService } from '@scullyio/ng-lib';

@Component({
  selector: 'app-user-profile',
  template: '<div>{{ username }}</div>'
})
export class UserProfileComponent implements OnInit {
  username: string;

  constructor(private transferState: TransferStateService) {}

  ngOnInit() {
    this.transferState.getState<string>('currentUser')
      .subscribe(username => {
        this.username = username;
      });
  }
}

setState()

Sets a value in the transfer state. This is typically used during Scully’s rendering process.
setState<T>(name: string, val: T): void
Parameters:
  • name - The key name for the state
  • val - The value to store
Example:
import { Component } from '@angular/core';
import { TransferStateService } from '@scullyio/ng-lib';

@Component({
  selector: 'app-data-provider'
})
export class DataProviderComponent {
  constructor(private transferState: TransferStateService) {}

  saveUserData(userData: any) {
    this.transferState.setState('userData', userData);
  }
}

stateHasKey()

Checks if the current state has a value for the given key name.
stateHasKey(name: string): boolean
Parameters:
  • name - The key name to check
Returns: true if the key exists (even if the value is undefined)

stateKeyHasValue()

Checks if the current state has a non-null value for the given key name.
stateKeyHasValue(name: string): boolean
Parameters:
  • name - The key name to check
Returns: true if the key exists and has a non-null value Example:
import { Component, OnInit } from '@angular/core';
import { TransferStateService } from '@scullyio/ng-lib';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-conditional-load'
})
export class ConditionalLoadComponent implements OnInit {
  constructor(
    private transferState: TransferStateService,
    private http: HttpClient
  ) {}

  ngOnInit() {
    if (this.transferState.stateKeyHasValue('config')) {
      // Use cached config
      const config = this.transferState.getState('config');
    } else {
      // Load fresh config
      this.http.get('/api/config').subscribe(config => {
        this.transferState.setState('config', config);
      });
    }
  }
}

How It Works

During Scully Rendering

  1. Your Angular app runs in Puppeteer
  2. When you call setState() or use useScullyTransferState(), the data is captured
  3. Scully embeds this data in the static HTML as a script tag
  4. The page is saved with the embedded state

On the Client

  1. When a user visits the static page, the embedded state is immediately available
  2. getState() returns the cached data synchronously on initial load
  3. No API calls are made for data that’s already in the state
  4. On route changes, new state is fetched from the next route’s data file or inline HTML

Configuration Options

The TransferState service behavior can be configured in scully.config.ts:
export const config: ScullyConfig = {
  // Store state inline in HTML instead of separate data.json files
  inlineStateOnly: true
};

Best Practices

Always use unique keys for different data to avoid conflicts. Include identifiers like IDs in your keys:
this.transferState.useScullyTransferState(
  `post-${postId}`,  // Unique per post
  this.http.get(`/api/posts/${postId}`)
);
Use useScullyTransferState() instead of manually calling getState() and setState(). It handles both development and production scenarios automatically.
Even with TransferState, handle loading states in your templates:
<div *ngIf="data$ | async as data; else loading">
  {{ data }}
</div>
<ng-template #loading>Loading...</ng-template>
Remember that state is embedded in the static HTML and visible to all users. Never store sensitive information like API keys or personal data.

Advanced Example

Here’s a complete example showing TransferState with a list and detail view:
// blog.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { TransferStateService } from '@scullyio/ng-lib';
import { Observable } from 'rxjs';

interface BlogPost {
  id: number;
  title: string;
  content: string;
  author: string;
}

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

  getAllPosts(): Observable<BlogPost[]> {
    return this.transferState.useScullyTransferState(
      'blog-posts',
      this.http.get<BlogPost[]>('/api/posts')
    );
  }

  getPost(id: number): Observable<BlogPost> {
    return this.transferState.useScullyTransferState(
      `blog-post-${id}`,
      this.http.get<BlogPost>(`/api/posts/${id}`)
    );
  }
}

// blog-list.component.ts
@Component({
  selector: 'app-blog-list',
  template: `
    <div *ngFor="let post of posts$ | async">
      <h2>{{ post.title }}</h2>
      <a [routerLink]="['/blog', post.id]">Read more</a>
    </div>
  `
})
export class BlogListComponent implements OnInit {
  posts$: Observable<BlogPost[]>;

  constructor(private blogService: BlogService) {}

  ngOnInit() {
    this.posts$ = this.blogService.getAllPosts();
  }
}

// blog-detail.component.ts
@Component({
  selector: 'app-blog-detail',
  template: `
    <article *ngIf="post$ | async as post">
      <h1>{{ post.title }}</h1>
      <p>By {{ post.author }}</p>
      <div>{{ post.content }}</div>
    </article>
  `
})
export class BlogDetailComponent implements OnInit {
  post$: Observable<BlogPost>;

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

  ngOnInit() {
    const id = +this.route.snapshot.params['id'];
    this.post$ = this.blogService.getPost(id);
  }
}

Source Code

View the complete source code:

Scully Library

Learn about the Angular library overview

Idle Monitor

Understand when Scully renders your pages

Build docs developers (and LLMs) love