Platform: Design and Implement Universal support.

Created on 19 Jul 2017  路  27Comments  路  Source: ngrx/platform

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.

Store enhancement

Most helpful comment

This is how I did it.

```typescript
// make sure you export for AoT
export function stateSetter(reducer: ActionReducer): 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({
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(NGRX_STATE);

    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));
}

}

All 27 comments

Spitballing an idea of how we might tackle this:

  1. Add a metareducer to handle re-hydration on the browser:
function universalMetaReducer(actionReducer) {
  return function (state, action) {
    if (action.type === 'REHYDRATE') {
      return actionReducer(action.state, action);
    }

    return actionReducer(state, action);
  }
}
  1. Create a component that serializes and rehydrates state depending on the platform. You would use this once in the root of your application:
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): 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({
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(NGRX_STATE);

    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?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

smorandi picture smorandi  路  3Comments

ghost picture ghost  路  3Comments

shyamal890 picture shyamal890  路  3Comments

mappedinn picture mappedinn  路  3Comments

brandonroberts picture brandonroberts  路  3Comments