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.