Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Design and Implement Universal support. #101

Closed
robwormald opened this issue Jul 19, 2017 · 28 comments
Closed

Design and Implement Universal support. #101

robwormald opened this issue Jul 19, 2017 · 28 comments

Comments

@robwormald
Copy link
Contributor

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.

@MikeRyanDev
Copy link
Member

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

@robwormald
Copy link
Contributor Author

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 👍

@kylecordes
Copy link

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.

@robwormald
Copy link
Contributor Author

@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)...

@MikeRyanDev
Copy link
Member

@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.

@karptonite
Copy link
Contributor

@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.

@MikeRyanDev
Copy link
Member

@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.

@Flood
Copy link

Flood commented Sep 5, 2017

Any updates on this?

@Toxicable
Copy link

Toxicable commented Sep 5, 2017

@Flood
I have a proposal here angular/universal#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.

@mcferren
Copy link

mcferren commented Sep 5, 2017

I also am very interesting in using this. stateTransfer object would replace initial state arg in the reducer function call?

@RSginer
Copy link

RSginer commented Sep 7, 2017

any updates? im very interested to use this

@Toxicable
Copy link

@RSginer it's currently in design

@RSginer
Copy link

RSginer commented Sep 10, 2017

@Toxicable okey thanks, if i can help you tell me!

@sarunint
Copy link

StateTransfer API has been merged: angular/angular#19134

@vikerman
Copy link

vikerman commented Oct 2, 2017

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.

@sjogren
Copy link

sjogren commented Oct 3, 2017

Any updates on this? Is anyone working on it?

@MattiJarvinen
Copy link

This is how I did it.

// make sure you export for AoT
export function stateSetter(reducer: ActionReducer<any>): ActionReducer<any> {
  return function(state: any, action: any) {
      if (action.type === 'SET_ROOT_STATE') {
          return action.payload;
      }
      return reducer(state, action);
  };
}

const _metaReducers: MetaReducer<fromRoot.State, any>[] = [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<fromRoot.State>,
    ) {
        const isBrowser = this.transferState.hasKey<any>(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));
    }
 }

@rijine
Copy link

rijine commented Feb 20, 2018

@MattiJarvinen-BA Do you have any repo having the code base ?

@MattiJarvinen
Copy link

@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

@renestalder
Copy link

@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?

@kaitlynekdahl
Copy link

kaitlynekdahl commented Mar 22, 2018

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

@renestalder
Copy link

@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?

@MattiJarvinen
Copy link

@renestalder that would be with import { ROOT_EFFECTS_INIT } from '@ngrx/effects';

Please do share your snippet when you got something to show.

@newprogrammer1991
Copy link

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

@mehrad-rafigh
Copy link

How would one solve this with the new creators syntax? Could not find any updated infos on that. Much appreciated

@puneetv05
Copy link

puneetv05 commented Jun 21, 2020

Hey @MattiJarvinen , want to know how should we handle the State that are forFeature for ex. StoreModule.forFeature('home', homeReducer) some part of root state was reset by "@ngrx/store/update-reducers"

@david-shortman
Copy link
Contributor

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?

@matthewdickinson
Copy link

@david-shortman As Universal is a first-class, Angular-supported feature/tool, I think it would be great to have first-class support in NgRx. However, since that's probably not going to be trivial to include in a generic sort of way that would work in everyone's setups, I think it should just be noted in the NgRx docs (cleaned up examples would be ideal, but just with a link to this thread would be super-helpful). Right now, the silence in the docs make it seems to me like 1) it should work out of the box or 2) that it's just fully unsupported.

Thanks!

@ngrx ngrx locked and limited conversation to collaborators Jul 20, 2021

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Projects
None yet
Development

No branches or pull requests