To use SSR in Angular or any other frontend framework, the challenge is generally avoiding duplicate API calls on the client side after the hydration. This typically happens because:
- The server makes API calls to render the page.
- After hydration, the client re-renders the app, often triggering the same API calls again.
To prevent double API calls, the recommended approach is to transfer server-fetched data to the client using Angular’s built-in TransferState API.
Recommended Solution: Use TransferState for SSR Data Caching
Angular provides a utility called TransferState from @angular/platform-browser, which allows injecting data during SSR and retrieving it on the client without making another API call.
Step-by-Step Example
Let’s walk through a concrete example of how to use TransferState to share API data from SSR to the client.
Service to Fetch Data
// data.service.ts
import { Injectable, inject, PLATFORM_ID } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { makeStateKey, TransferState } from '@angular/platform-browser';
import { isPlatformServer } from '@angular/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable({ providedIn: 'root'
})
export class DataService { private http = inject(HttpClient); private transferState = inject(TransferState); private platformId = inject(PLATFORM_ID); private DATA_KEY = makeStateKey<any>('api-data'); getData(): Observable<any> { // If running on the client and state exists, return it if (!isPlatformServer(this.platformId) && this.transferState.hasKey(this.DATA_KEY)) { const data = this.transferState.get(this.DATA_KEY, null); this.transferState.remove(this.DATA_KEY); // Clean up after use return of(data); } // Fetch from API return this.http.get('https://api.example.com/data').pipe( tap(data => { // If on the server, save the data to TransferState if (isPlatformServer(this.platformId)) { this.transferState.set(this.DATA_KEY, data); } }) ); }
}
Component Using the Service
// app.component.ts
import { Component, OnInit } from '@angular/core';
import { DataService } from './data.service';
@Component({ selector: 'app-root', template: `<h1>SSR Data</h1><pre>{{ data | json }}</pre>`
})
export class AppComponent implements OnInit { data: any; constructor(private dataService: DataService) {} ngOnInit() { this.dataService.getData().subscribe(res => { this.data = res; }); }
}
How It Works:
- During SSR, Angular executes getData(), makes the API call, and stores the result in TransferState.
- The rendered HTML includes a <script> tag with serialized data.
- When the browser hydrates the page, Angular reads from TransferState instead of re-calling the API.
Why Use TransferState?
- Prevents duplicate HTTP calls
- Increases performance by avoiding unnecessary network traffic
- Improves user experience by displaying the data immediately on load
- Built into Angular with no external dependencies
Best Practices and Tips
- Always use makeStateKey<T>() for strong typing and uniqueness.
- Remove data from TransferState after reading it on the client to free memory.
- Use platform checks (isPlatformServer, isPlatformBrowser) to conditionally execute logic.
Common Pitfalls
Not using TransferState results in:
- SSR making an API call
- Client making the same API call again
Conclusion
Using Angular’s TransferState API is an effective way to prevent duplicate API calls after SSR hydration. To implement this and other best practices in your projects, you can always hire Angular developers to improve performance and ensure a smoother user experience.