9

I'm trying to get TransferState working in my Angular Universal v5 app. After a lot of debugging, I've realized that, on the server, TransferState seems to be working as the response includes a <script id=APP_ID></script> tag which contains the appropriate serialized state (TransferState source).

For some reason, the browser app isn't being initialized with the state however. If I hardcode a test state into my app's index.html file (via copy & pasting), then my browser app is successfully initialized with the browser state. I'm not sure what is going wrong. Any ideas a much appreciated!

My only guess, is that when I watch my app load in Chrome's inspector, it appears as though the majority of elements are loaded at one time, and then in another tick the <script id=APP_ID></script> shows up. This seems to imply that the script tag is being generated/processed on the browser side in some way, even though I've inspected the server's response and it includes the script tag. If the script tag was going through some sort of processing on the client side however, perhaps TransferState is being initialized before that processing is complete, which is why my app isn't receiving the state. Again, any ideas are much appreciated!

Here is the relevant code:

app.module.ts

import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser';
import { environment } from '../environments/environment';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';    
import { AppRoutingModule } from './app-routing.module';
import { GraphQLModule } from './graphql.module';

@NgModule({
  imports: [
    BrowserModule.withServerTransition({ appId: 'service-work-coordination' }),
    BrowserTransferStateModule,
    AppRoutingModule,
    GraphQLModule,
  ],
  declarations: [AppComponent],
  bootstrap: [AppComponent],
})
export class AppModule {}

app.server.module.ts

import { NgModule } from '@angular/core';
import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';

import { AppModule } from './app.module';
import { AppComponent } from './app.component';

@NgModule({
  imports: [
    AppModule,
    ServerModule,
    ServerTransferStateModule,
    ModuleMapLoaderModule, // <-- *Important* to have lazy-loaded routes work
  ],
  // Since the bootstrapped component is not inherited from your
  // imported AppModule, it needs to be repeated here.
  bootstrap: [AppComponent],
})
export class AppServerModule {}

graphql.module.ts

import { NgModule, APP_ID, PLATFORM_ID, Inject } from '@angular/core';
import { TransferState, makeStateKey } from '@angular/platform-browser';
import { HttpClientModule, HttpHeaders } from '@angular/common/http';
import { environment } from '../environments/environment';

// Apollo
import { ApolloModule, Apollo } from 'apollo-angular';
import { HttpLinkModule, HttpLink, HttpLinkHandler } from 'apollo-angular-link-http';
import { InMemoryCache, NormalizedCache, NormalizedCacheObject } from 'apollo-cache-inmemory';
import { setContext } from 'apollo-link-context';
import { isPlatformBrowser } from '@angular/common';

const uri = environment.uris.api.graphql;

const STATE_KEY = makeStateKey<any>('apollo.state');

@NgModule({
  imports: [
    HttpClientModule,
    ApolloModule,
    HttpLinkModule,
  ],
  exports: [
    HttpClientModule,
    ApolloModule,
    HttpLinkModule,
  ]
})
export class GraphQLModule {
  private cache: InMemoryCache;
  private link: HttpLinkHandler;

  constructor(
    private apollo: Apollo,
    private transferState: TransferState,
    private httpLink: HttpLink,
    @Inject(PLATFORM_ID) private platformId: any,
  ) {
    this.cache = new InMemoryCache();
    this.link = this.httpLink.create({ uri });

    console.log('transferState: ', this.transferState);

    const isBrowser = this.transferState.hasKey<NormalizedCache>(STATE_KEY);

    if (isPlatformBrowser(this.platformId)) {
      this.apollo.create({
        link: this.link,
        cache: this.cache,
        ssrForceFetchDelay: 100,
      });

      this.onBrowser();      
    } else {      
      this.apollo.create({
        link: this.link,
        cache: this.cache,
        ssrMode: true,
      });

      this.onServer();      
    }
  }

  onServer(): void {
    this.transferState.onSerialize(STATE_KEY, () => this.cache.extract());
  }

  onBrowser(): void {
    const state = this.transferState.get<NormalizedCacheObject | null>(STATE_KEY, null);

    if (state) {
      this.cache.restore(state);      
    }
  }
}

Simplified server response

<html>
<head>...app code...</head>
<body>
<app-root>...app code...</app-root>
<script type="text/javascript" src="inline.6ce41075b82d3dba433b.bundle.js"></script>
<script type="text/javascript" src="polyfills.37cc021a2888e752595b.bundle.js"></script>
<script type="text/javascript" src="main.1efdc21cec25daa396d1.bundle.js"></script>
<script id="service-work-coordination-state" type="application/json">{&q;apollo.state&q;:{&q;$ROOT_QUERY.person({\&q;id\&q;:\&q;074a9421-53bb-44c7-8afe-43130c723bd9\&q;})&q;:{&q;firstName&q;:&q;John&q;,&q;middleName&q;:null,&q;lastName&q;:&q;Carroll&q;,&q;primaryEmailAddress&q;:&q;:`EmailAddress::Person::Current`:`EmailAddress::Person`:`EmailAddress::Current`:`EmailAddress`:`Current` {uuid: &s;f0c4896a-27da-410b-84d3-3d66e1507a7e&s;}&q;,&q;__typename&q;:&q;Person&q;},&q;ROOT_QUERY&q;:{&q;person({\&q;id\&q;:\&q;074a9421-53bb-44c7-8afe-43130c723bd9\&q;})&q;:{&q;type&q;:&q;id&q;,&q;id&q;:&q;$ROOT_QUERY.person({\&q;id\&q;:\&q;074a9421-53bb-44c7-8afe-43130c723bd9\&q;})&q;,&q;generated&q;:true}}}}</script>
</body>
</html>
JonathanDavidArndt
  • 2,518
  • 13
  • 37
  • 49
John
  • 9,249
  • 5
  • 44
  • 76

1 Answers1

23

Uggg... So it turns out I've been running into a known (thankfully) bug in Angular. TransferState on the client is being initiated while the script tag is being processed / loaded in some way. To get around this, currently you need to delay bootstrapping of the angular client side app.

update main.ts

document.addEventListener('DOMContentLoaded', () => {
  platformBrowserDynamic()
  .bootstrapModule(AppModule)
});
John
  • 9,249
  • 5
  • 44
  • 76
  • 2
    Many thanks, you saved my day! I wonder why Angular team doesn't include this information in their official document (https://angular.io/api/platform-browser/TransferState). – kimamula Sep 19 '18 at 16:08
  • 1
    @kimamula hu, you're quite right! When I created this issue, `TransferState` was new and the related issue in the Angular repo was new. I just assumed they were going to fix the problem promptly and a docs update wasn't really needed. That was a mistake on my part. I just created a [PR to update the docs](https://github.com/angular/angular/pull/26026). I do wonder why you, and no one else viewing this issue in the past year, didn't create a PR to update the docs though. I'm guessing the answer to that is why the angular team also didn't. – John Sep 20 '18 at 00:30
  • For that matter, the continued +1's I've received for this issue should have tipped me off that it was still unresolved and I should make a docs PR. Lots of fails all around. – John Sep 20 '18 at 00:40
  • 2
    I added SSR support to my application using CLI, but it has this problem, it took me around 3 days to realize what was the problem. Thanks for this useful answer – Mohammad Kermani Jul 10 '19 at 06:35
  • @M98 that sounds incredibly frustrating! The few hours I spent on the problem was bad enough! I'm in a slow, [protracted conversation with the Angular team](https://github.com/angular/angular/pull/26026) trying to get them to improve the documentation in this area so that future devs don't need to waste their time like we did. Surprisingly, the Angular team has taken the stance that "the docs are good enough as-is". Maybe if a few more people [+1 the PR](https://github.com/angular/angular/pull/26026) they can be persuaded otherwise. – John Jul 10 '19 at 08:35
  • 1
    @John I did, and I also reviewed and added a comment to support the PR. – Mohammad Kermani Jul 12 '19 at 10:16