Closed John0x closed 5 years ago
I have no experience with SSR, can you write this up for the community?
Basically you gotta polyfill or check if browser specific things exists before start using them, like the localStorage
, it is not available in the server and throws an error when running in the server. The NgxsStoragePluginModule
is one guy who breaks SSR because of localStorage
usage.
For example, in these lines: https://github.com/ngxs/store/blob/b42b4a405a3b11fbdf4243c4a3821228c069477d/packages/storage-plugin/src/storage.module.ts#L23-L31
We should verify if both localStorage
and sessionStorage
are defined before returning any, otherwise we get errors like that:
$ yarn serve:universal
yarn run v1.7.0
$ cd dist && node server
Node Express server listening on http://localhost:4000
ReferenceError: localStorage is not defined
at engineFactory (\path\to\my\project\Havan.Vc.Interface.PWA\dist\server.js:228592:9)
at _callFactory (\path\to\my\project\Havan.Vc.Interface.PWA\dist\server.js:12348:20)
at _createProviderInstance (\path\to\my\project\Havan.Vc.Interface.PWA\dist\server.js:12304:26)
at initNgModule (\path\to\my\project\Havan.Vc.Interface.PWA\dist\server.js:12234:32)
at new NgModuleRef_ (\path\to\my\project\Havan.Vc.Interface.PWA\dist\server.js:12960:9)
at Object.createNgModuleRef (\path\to\my\project\Havan.Vc.Interface.PWA\dist\server.js:12949:12)
at NgModuleFactory_.create (\path\to\my\project\Havan.Vc.Interface.PWA\dist\server.js:15491:25)
at \path\to\my\project\Havan.Vc.Interface.PWA\dist\server.js:8221:43
at ZoneDelegate.invoke (\path\to\my\project\Havan.Vc.Interface.PWA\dist\server.js:533:26)
at Object.onInvoke (\path\to\my\project\Havan.Vc.Interface.PWA\dist\server.js:7732:33)
Workaround for localStorage
in server-side:
// app.module
// @angular
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
// @env
import { environment } from '@env/environment';
// State management
import { NgxsModule } from '@ngxs/store';
import { NgxsStoragePluginModule } from '@ngxs/storage-plugin';
import { NgxsLoggerPluginModule } from '@ngxs/logger-plugin';
import { NgxsReduxDevtoolsPluginModule } from '@ngxs/devtools-plugin';
import { AppComponent } from '@app/app.component';
import { AppSettingsState } from '@shared/state/app-settings.state';
/**
* Main Module class.
*/
@NgModule({
declarations: [
// Components
AppComponent,
// ...
],
imports: [
// Application
BrowserModule.withServerTransition({ appId: 'com.myapp' }),
// State management
NgxsModule.forRoot([
AppSettingsState
]),
NgxsStoragePluginModule.forRoot(),
NgxsLoggerPluginModule.forRoot({
disabled: environment.production
}),
NgxsReduxDevtoolsPluginModule.forRoot({
name: 'MY_APP_STORE',
disabled: environment.production
}),
// ...
],
providers: [
// ...
],
bootstrap: [AppComponent]
})
export class AppModule {}
// app.server.module
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { AppModule } from '@app/app.module';
import { STORAGE_ENGINE } from '@ngxs/storage-plugin';
import { LOCAL_STORAGE } from '@shared/local-storage.token';
import { LocalStoragePolyfill } from '@shared/local-storage.polyfill';
@NgModule({
imports: [
// The AppServerModule should import your AppModule followed
// by the ServerModule from @angular/platform-server.
AppModule,
ServerModule,
// ...
],
providers: [
// ...
// replaces NGXS storage engine on server
{ provide: STORAGE_ENGINE, useClass: LocalStoragePolyfill },
// you can also create a token for yourself
{ provide: LOCAL_STORAGE, useClass: LocalStoragePolyfill }, <-- PROVIDE YOUR CUSTOM LOCAL STORAGE POLYFILL
],
// Since the bootstrapped component is not inherited from your
// imported AppModule, it needs to be repeated here.
bootstrap: [AppComponent],
})
export class AppServerModule { }
export class LocalStoragePolyfill {
private _data = {};
constructor() {
this._data = {};
}
setItem(id, val: string) {
return this._data[id] = String(val);
}
getItem(id) {
return this._data.hasOwnProperty(id) ? this._data[id] : null;
}
removeItem(id) {
delete this._data[id];
}
clear() {
return this._data = {};
}
}
https://github.com/Angular-RU/angular-universal-starter/tree/ngxs - problem with HTTP transfer state - we send get HTTP request twice(server+browser).
@Gorniv you should use TransferHttpCacheModule
to avoid duplicated requests between server and browser.
Check this https://github.com/angular/universal/blob/master/docs/transfer-http.md
BTW, make sure to import this module before any other interceptor which may change req.url
, or it won't work.
@clarkorz problem isn't in HTTP. ssr dosn't wait complete store.dispatch(new Action());
https://ssr.angular.su/back (delay 3 sec for http get)
and https://github.com/Angular-RU/angular-universal-starter/blob/dc632fbc46278d1486346dc432ff17a982b6fc09/src/app/transfer-back/transfer-back.component.ts#L21 work good.
any idea?
Not from me this time. :/
Is there a solution to make it works today ?
There is not only this problem, when an action is dispatched, the universal renderer doesn't wait the end of this action to render..... So a basic init dispatch doesn't work and I tried a lot of solutions.... NgRx works with SSR, and I don't know what to do...
You can try: npm i @ngxs/store@dev -D
Is the problem solved?
Yes it solved, the problem :) Nice thank you :)
is there other way to fix the localstorage error other that provide local storage polyfill?
@wunaidage open a new issue with reproduce
There is not only this problem, when an action is dispatched, the universal renderer doesn't wait the end of this action to render..... So a basic init dispatch doesn't work and I tried a lot of solutions.... NgRx works with SSR, and I don't know what to do...
This still not working...
@call-me-adas create repo for reproduce
We have integration test with SSR in @ngxs/store
repo and it works
Feature Request
It would be great to have some builtin way to handle SSR or at least have a section in the docs for it