The Problem
Angular Universal's TransferState is designed to avoid duplicate API calls by transferring data from the server render to the browser hydration. When it does not work, the browser makes the same API calls again, defeating the purpose of SSR.
Symptoms
- API called twice: once on server, once on browser
- Page load is slow despite SSR
- TransferState has() returns false in browser
- Server renders correctly but browser re-fetches everything
Real Error Scenario
```typescript // data.resolver.ts import { TransferState, makeStateKey } from '@angular/platform-browser';
const DATA_KEY = makeStateKey('api-data');
@Injectable() export class DataResolver implements Resolve<any> { constructor( private http: HttpClient, private transferState: TransferState ) {}
resolve(): Observable<any> { // WRONG: Not checking TransferState first return this.http.get('/api/data'); // Called on BOTH server and browser } } ```
How to Fix It
Fix 1: Proper TransferState Pattern
```typescript import { TransferState, makeStateKey, isPlatformBrowser } from '@angular/common'; import { Inject, PLATFORM_ID } from '@angular/core';
const DATA_KEY = makeStateKey('api-data');
@Injectable() export class DataResolver implements Resolve<any> { constructor( private http: HttpClient, private transferState: TransferState, @Inject(PLATFORM_ID) private platformId: Object ) {}
resolve(): Observable<any> { // Check if data was transferred from server if (this.transferState.has(DATA_KEY)) { const data = this.transferState.get(DATA_KEY, null); this.transferState.remove(DATA_KEY); // Clean up return of(data); }
return this.http.get('/api/data').pipe( tap(data => { // Only set on server, not browser if (!isPlatformBrowser(this.platformId)) { this.transferState.set(DATA_KEY, data); } }) ); } } ```
Fix 2: Use State Transfer via Interceptor
```typescript @Injectable() export class TransferStateInterceptor implements HttpInterceptor { constructor( private transferState: TransferState, @Inject(PLATFORM_ID) private platformId: Object ) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const key = makeStateKey(http-${req.url});
if (this.transferState.has(key)) { const response = this.transferState.get(key, null); this.transferState.remove(key); return of(new HttpResponse({ body: response, status: 200 })); }
return next.handle(req).pipe( tap(event => { if (event instanceof HttpResponse && !isPlatformBrowser(this.platformId)) { this.transferState.set(key, event.body); } }) ); } } ```
Fix 3: Ensure Server Module Imports
// app.server.module.ts
@NgModule({
imports: [
AppSharedModule,
ServerModule,
ServerTransferStateModule // REQUIRED for SSR
],
bootstrap: [AppComponent]
})
export class AppServerModule {}Fix 4: Verify Browser Module
// app.browser.module.ts
@NgModule({
imports: [
AppSharedModule,
BrowserModule.withServerTransition({ appId: 'my-app' }),
BrowserTransferStateModule // REQUIRED for browser hydration
],
bootstrap: [AppComponent]
})
export class AppBrowserModule {}Debugging
// Add to app.component.ts
constructor(private transferState: TransferState) {
setTimeout(() => {
console.log('TransferState keys:', this.transferState as any);
});
}