Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular Universal TransferState not loading on Client

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>
like image 529
John Avatar asked Dec 03 '17 15:12

John


1 Answers

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)
});
like image 188
John Avatar answered Oct 25 '22 01:10

John