Now 4.0 and Universal are out in the wild, we should see about making state hydration a bit simpler, and enabling developers to as automagically-as-possible transfer initial state from client -> server.
Spitballing an idea of how we might tackle this:
function universalMetaReducer(actionReducer) {
return function (state, action) {
if (action.type === 'REHYDRATE') {
return actionReducer(action.state, action);
}
return actionReducer(state, action);
}
}
import { isPlatformBrowser } from '@angular/common';
@Component({
template: `
{{ state$ | json }}
`
})
export class NgrxUniversalComponent {
state$: Observable<any>;
constructor(private elementRef: ElementRef, private store: Store<any>) {
if (isPlatformBrowser()) {
this.startRehydration();
}
else {
this.startSerialization();
}
}
startRehydration() {
try {
const state = JSON.parse(this.elementRef.nativeElement.innerText);
this.store.dispatch({ type: 'REHYDRATE', state });
this.state$ = Observable.empty();
} catch(e) { }
}
startSerialization() {
this.state$ = this.store;
}
}
update - @vikerman advises me that we should have a built in for transferState in platform-server in the next couple of weeks, and we'll build off that 馃憤
I noticed that the snippets above use | json and JSON.parse. That will only handle plain JSON objects with the handful of primitive types, right?
Over in #143 and #160 it sounds like there is work underway that will make many Actions will have symbols in them, and will depend on the Action being of a specific class, not just an Object with the right data in it. Both of those things require a more complex serialization approach.
@kylecordes I don't think you'd be transferring actions this way, so non-serializable actions are probably fine (in this context). State is the thing being transferred here, which should almost always be serializable (the router state issue notwithstanding)...
@kylecordes I don't believe #143 or #160 will be going into ngrx. Classes are only used for actions to reduce the boilerplate of building type unions and action creators. Serialization is still a major focus for ngrx.
@MikeRyanDev That makes sense, but reducing boilerplate is not the only thing classes are used for. They are also used for type inference via the readonly type properties--I don't think we could have easily reproduced that behavior with plain objects and action creators, because you can't add a readonly property with an initialized value to an interface. Granted, that is only used compile-time. I had originally thought that ofClass fell into a similar category, but I guess the fact that it has run-time behavior makes it a bit more of an issue.
But I don't see class based actions, which we already use, interfering with state rehydration in universal, as long as the state itself remains serializable.
@karptonite readonly is merely a side-effect of using classes. If you assign a string constant to the type property of a class it will widen the type to a string. The readonly modifier keeps the type narrow.
Any updates on this?
@Flood
I have a proposal here https://github.com/angular/universal/issues/791 for the StateTransfer API along with draft implementations of each proposed API.
So it wouldn't take me long to actually implement it but I need other people on the team to sign off/review it.
If you want to help then voice your opinion over there.
I also am very interesting in using this. stateTransfer object would replace initial state arg in the reducer function call?
any updates? im very interested to use this
@RSginer it's currently in design
@Toxicable okey thanks, if i can help you tell me!
StateTransfer API has been merged: angular/angular#19134
This is in 5.0 RC now - https://next.angular.io/api/platform-browser/TransferState
You would want to hook on to TransferState.onSerialize to the set the current state object just before the DOM is serialized on the server side.
Any updates on this? Is anyone working on it?
This is how I did it.
```typescript
// make sure you export for AoT
export function stateSetter(reducer: ActionReducer
return function(state: any, action: any) {
if (action.type === 'SET_ROOT_STATE') {
return action.payload;
}
return reducer(state, action);
};
}
const _metaReducers: MetaReducer
if ( !environment.production ) {
_metaReducers.push( debugMetaReducer );
}
export const metaReducers = _metaReducers;
export const NGRX_STATE = makeStateKey('NGRX_STATE');
const modules = [
StoreModule.forRoot(fromRoot.reducers, { metaReducers }),
HttpClientModule,
RouterModule,
routing,
BrowserModule.withServerTransition({
appId: 'store-app'
}),
BrowserTransferStateModule,
];
const services = [
{
provide: RouterStateSerializer,
useClass: MyRouterStateSerializer,
}
];
const containers = [
AppComponent,
HomeComponent,
];
@NgModule({
bootstrap: [AppComponent],
declarations: [
...containers
],
imports: [
...modules,
BrowserModule.withServerTransition({ appId: 'store-app' }),
StoreRouterConnectingModule,
],
providers: [
...services,
],
})
export class AppModule {
public constructor(
private readonly transferState: TransferState,
private readonly store: Store
) {
const isBrowser = this.transferState.hasKey
if (isBrowser) {
this.onBrowser();
} else {
this.onServer();
}
}
onServer() {
this.transferState.onSerialize(NGRX_STATE, () => {
let state;
this.store.subscribe( ( saveState: any ) => {
// console.log('Set for browser', JSON.stringify(saveState));
state = saveState;
}).unsubscribe();
return state;
});
}
onBrowser() {
const state = this.transferState.get<any>(NGRX_STATE, null);
this.transferState.remove(NGRX_STATE);
this.store.dispatch( { type: 'SET_ROOT_STATE', payload: state } );
// console.log('Got state from server', JSON.stringify(state));
}
}
@MattiJarvinen-BA Do you have any repo having the code base ?
@rijine everything related to transitioning server state to browser is in the code above. Basic angular server side render tutorials can be found on Angular.io https://angular.io/guide/universal
@MattiJarvinen-BA's solution is cool. I just wonder, when you use effects, wouldn't you rather set and retrieve the transfer state in the effect where you make the service calls to the API?
@renestalder I'm doing something like this to first check TransferState for data, then calling the service if there's nothing there.
@Effect()
loadsProducts$ = this.actions$.ofType(productActions.LOAD_PRODUCTS)
.pipe(
switchMap(() => {
const productList = this.transferState.get(PRODUCTS_KEY, [] as any);
if (this.transferState.hasKey(PRODUCTS_KEY) && productList.length > 0) {
// products key found in transferstate, use that
return of(this.transferState.get(PRODUCTS_KEY, [] as any))
.pipe(
map(stateValue => new productActions.LoadSuccess(stateValue))
);
}
else {
// products key NOT found, calling service
return this.myService.getProducts()
.pipe(
map(products => new productActions.LoadSuccess(products)),
catchError(error => of(new productActions.LoadFail(error)))
}
})
);
@kaitlynekdahl Nice, thanks. Will try to implement that in one general effect running before the other effects to only be needed doing it once.
Where do you put transferState.remove? Wouldn't your current code always point to the transfer state version of your data when the state isn't removed after dispatched to the store?
@renestalder that would be with import { ROOT_EFFECTS_INIT } from '@ngrx/effects';
Please do share your snippet when you got something to show.
please help with ngrx-router - I have router error with
this.transferState.onSerialize(NGRX_STATE, () => {
let state;
this.store.subscribe( ( saveState: any ) => {
// console.log('Set for browser', JSON.stringify(saveState));
state = saveState;
}).unsubscribe();
return state;
});
}
How would one solve this with the new creators syntax? Could not find any updated infos on that. Much appreciated
Hey @MattiJarvinen , want to know how should we handle the State that areforFeature for ex. StoreModule.forFeature('home', homeReducer) some part of root state was reset by "@ngrx/store/update-reducers"
Based on the conversation, it seems NgRx is usable within Universal with some extra setup.
Would it be a goal of the project to give Universal first-class support, or is the demonstrated user-land capability to integrate sufficient?
Most helpful comment
This is how I did it.
```typescript): ActionReducer {
// make sure you export for AoT
export function stateSetter(reducer: ActionReducer
return function(state: any, action: any) {
if (action.type === 'SET_ROOT_STATE') {
return action.payload;
}
return reducer(state, action);
};
}
const _metaReducers: MetaReducer[] = [stateSetter];
if ( !environment.production ) {
_metaReducers.push( debugMetaReducer );
}
export const metaReducers = _metaReducers;
export const NGRX_STATE = makeStateKey('NGRX_STATE');
const modules = [
StoreModule.forRoot(fromRoot.reducers, { metaReducers }),
HttpClientModule,
RouterModule,
routing,
BrowserModule.withServerTransition({
appId: 'store-app'
}),
BrowserTransferStateModule,
];
const services = [
{
provide: RouterStateSerializer,
useClass: MyRouterStateSerializer,
}
];
const containers = [
AppComponent,
HomeComponent,
];
@NgModule({,(NGRX_STATE);
bootstrap: [AppComponent],
declarations: [
...containers
],
imports: [
...modules,
BrowserModule.withServerTransition({ appId: 'store-app' }),
StoreRouterConnectingModule,
],
providers: [
...services,
],
})
export class AppModule {
public constructor(
private readonly transferState: TransferState,
private readonly store: Store
) {
const isBrowser = this.transferState.hasKey
}