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);
}
}
}
<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>
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.
main.ts
document.addEventListener('DOMContentLoaded', () => {
platformBrowserDynamic()
.bootstrapModule(AppModule)
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With