Skip to content
This repository has been archived by the owner on Jan 10, 2018. It is now read-only.

Modules, lazy-loading and multiple stores? #197

Closed
mstawick opened this issue Aug 23, 2016 · 19 comments
Closed

Modules, lazy-loading and multiple stores? #197

mstawick opened this issue Aug 23, 2016 · 19 comments

Comments

@mstawick
Copy link

After migration to RC5, is there a way to have multiple stores? My app is composed of several modules, some of them lazy loaded.

Since services provided in @NgModule-s ale globally scoped, right now I need to have one store which will hold all the data for all modules together with all the reducers. From design perspective it would make more sense to keep (at least) reducers together with respective modules, but then I would have to give up on lazy loading.

So, is there a way to have stores provided per module, ie. multiple global instances of store, using different models? Or maybe provide them at the scope of modules root @component?

@MikeRyanDev
Copy link
Member

@ngrx/store is not opinionated about how you handle lazy loading reducers. However, this doesn't mean it isn't possible. All of the necessary APIs exist to let you dynamically add reducers.

Since this question comes up fairly regularly here's a game plan that you might follow in your app:

In this example I am making an app that has authentication. I have an auth reducer to track the authentication state and a user reducer to track the user's state. These two pieces of state should always be stored regardless of where the user is in the application.

One major feature of my app is that users can create a blog. All of the code to handle this feature is asynchronously loaded and all of its state only needs to be stored when the user is on this feature. This means I don't want to load the blog reducer unless necessary.

To achieve this I'm going to make a function called createReducer that will combine my auth reducer and user reducer together and can take an optional map of additional reducers to combine with them. Using this reducer factory function I can also make the root app reducer:

import { ActionReducer, combineReducers } from '@ngrx/store';
import { AuthState, authReducer } from './reducers/auth';
import { UserState, userReducer } from './reducers/user'; 

export interface AppState {
  auth: AuthState;
  user: UserState;
}

export function createReducer(asyncReducers = {}): ActionReducer<any> {
  return combineReducers(Object.assign({
    auth: authReducer,
    user: userReducer,
    // any other reducers you always want to be available
  }, asyncReducers));
}

export const appReducer = createReducer();

The appReducer is what I will pass to StoreModule.provideStore():

import { appReducer } from './reducers';

@NgModule({
  imports: [
    StoreModule.provideStore(appReducer)
  ]
})
export class AppModule { }

For my blog feature I'm going to define the reducers in the blog feature folder. When we are on the blog feature we want to augment app state with an additional key blog that tracks our blog state. We also need to make a new reducer blogReducer to manage this slice of state and use our reducer factory function createReducer to make a new root reducer:

import { Action } from '@ngrx/store';
import { createReducer, AppState } from '../base/reducer';

export interface BlogState {
  posts: any[];
}

export interface AppStateWithBlog extends AppState {
  blog: BlogState;
}

export function blogReducer(state: BlogState, action: Action): BlogState {
  // blog reducer implementation
}

export const appReducerWithBlog = createReducer({ blog: blogReducer });

The BlogPageComponent is the root page of this feature. In the constructor of this component I am going to swap out the root reducer with my new augmented reducer:

import { AppStateWithBlog, appReducerWithBlog } from '../blog/reducer';

@Component({
  selector: 'blog-page',
  template: `...`
})
export class BlogPageComponent {
  constructor(private store: Store<AppStateWithBlog>) {
    store.replaceReducer(appReducerWithBlog);
  }
}

Now BlogPageComponent and all of its children have access to blog state. One benefit to the above approach is that if I visit additional features that also cause the reducer to be replaced my blog state will be automatically cleaned up.

This is just one approach to implementing async reducers in your app. There are probably a few other ways you could tackle this problem. Hope this helps!

@mstawick
Copy link
Author

Thank you very much for comprehensive answer, really appreciated!

@fxck
Copy link

fxck commented Sep 8, 2016

@MikeRyan52 we should create a FAQ and include this there.. it keeps coming up on gitter. Daily.

@jeremyputeaux
Copy link

+1 :)

@nicohabets
Copy link

How can you prevent to cleanup of the blog state reducer?

@alexciesielski
Copy link

alexciesielski commented Oct 6, 2016

@MikeRyan52 's answer should be in the docs somewhere 💯

@johnbendi
Copy link

@MikeRyan52 my store is not being injected with BlogState. When I log the store state to console still shows the store with only AppState. Is there something else that needs to be done other than that which you outlined here?

@nweldev
Copy link

nweldev commented Jan 24, 2017

Based on the response given by @MikeRyan52 (#197 (comment)), I use the following :

function deepCombineReducers(reducers: any): ActionReducer<any> {

  Object.getOwnPropertyNames(reducers).forEach((prop) => {
    if (reducers.hasOwnProperty(prop)
      && reducers[prop] !== null
      && typeof reducers[prop] !== 'function') {
      reducers[prop] = deepCombineReducers(reducers[prop]);
    }
  });

  return combineReducers(reducers);
}

export function createReducer(asyncReducers = {}): ActionReducer<any> {
  return deepCombineReducers(Object.assign({
    router: routerReducer,
    // any other reducers you always want to be available
  }, asyncReducers));
}

export function appReducer(state: any, action: any) {
  return createReducer();
}

which permit me to have, in my opinion, a better naming convention for stores :

const appReducerWithHeroes = createReducer({
  heroes: {
    search: heroesSearchReducer,
    single: heroesSingleReducer
  },
});

export interface AppStateWithHeroes extends AppState {
  heroes: {
    search: HeroesSearchState,
    single: HeroesSingleState
  };
}

export class StoreWithHeroes extends Store<AppStateWithHeroes> {}

export function heroesStoreFactory(appStore: Store<AppState>) {
      appStore.replaceReducer(appReducerWithHeroes);
      return appStore;
}

and then :

constructor(private store: StoreWithHeroes) {
    this.heroes = store.select(s => s.heroes.search.result && s.heroes.search.result.heroes);
}

Tell me if you think this is a good or a bad approach.

FYI : I wanted to propose a PR to ngrx/store with recursive combineReducers, but I had a lot of troubles running the tests last week (see #321). I'll propose this as soon as I could resolve this, if you think this is a good idea (but this is a little out of topic).

@josh-sachs
Copy link

because there is a lot of useful information in this ticket....

I've adopted the pattern suggest by @noelmace but there was one issue.

I couldn't get noel's version defining appReducer as a function to work...

export function appReducer(state: any, action: any) {
  return createReducer();
}

I had to use MikeRyan52's defining appReducer as a constant of the createReducer() output.
export const appReducer = createReducer();

@crain
Copy link

crain commented Mar 17, 2017

@noelmace How are you using heroesStoreFactory in your example?

I was trying something like in my feature module:

providers: [ { provide: StoreWithHeroes, useFactory: heroesStoreFactory, deps: [Store]} ],
But this is loading the factory everytime my components are called which are injecting StoreWithHeros.

@josh-sachs
Copy link

Holy shit.. I think I cracked the code... @nolemace's technique will work for JIT and AoT... but there is a typo in his appReducer function.

It should be...

export function appReducer(state: any, action: any) {
  return createReducer()(state, action);
}

Turns out the result of createReducer() is an ActionReducer which is actually itself a function.

For completeness... this works with Angular 2 AoT and JIT from @noelmace:

export const EAGER_REDUCERS= { 
    x: xReducer,
    y: yReducer
    ...
};

export function deepCombineReducers(reducers: any): ActionReducer<any> {
    Object.getOwnPropertyNames(reducers).forEach((prop) => {
        if (reducers.hasOwnProperty(prop)
            && reducers[prop] !== null
            && typeof reducers[prop] !== 'function') {
            reducers[prop] = deepCombineReducers(reducers[prop]);
        }
    });
    return combineReducers(reducers);
}

export function createReducer(asyncReducers = {}) {    
    let allReducers = Object.assign(EAGER_REDUCERS, asyncReducers)
    return deepCombineReducers(allReducers); 
}

export function appReducer(state: any, action: any) {
    return createReducer()(state, action);
}

@NgModule({
...
imports: [StoreModule.provideStore(appReducer)],
...
})

@ksvitkovsky
Copy link

@nicohabets did you manage to solve persisting state of lazy modules?

I'm playing with lazy modules and tried the following approach to hold state of lazy modules on navigation between different routes. Although my code does work with just @ngrx/store itself, when I use @ngrx/store-devtools stub-reducers somehow lose the state.

I added stub-reducer that accepts the single parameter and returns it right away. That way when module is being loaded this node is being replaced with the actual reducer and after navigation our stub-reducer holds the last module state.

// app.module
const root = {
    auth: authReducer,
    user: userReducer,
    // stub-reducers
    someLazyState: (state) => state
};
const rootReducer = combineReducers(root);

// some-lazy.module
const state = {
     ...root,
     someLazyState: someLazyReducer
};

@fxck
Copy link

fxck commented Mar 29, 2017

Why don't you all just wait couple of days / weeks for v4 to be released on npm?

@tamasfoldi
Copy link

@fxck Are there any official informations about the next release?

@fxck
Copy link

fxck commented Apr 4, 2017

Nope, but it's already on master. Pretty much everyone on the ngrx team will be on ng-conf for the next couple of days, so I imagine they will finish docs publish it on npm some days after that.

Also all of the ngrx packages will be published under one mono repo, ngrx/platform.

@fxck
Copy link

fxck commented Apr 4, 2017

watch this talk https://www.ng-conf.org/sessions/inactive-reactive-ngrx/ and you might get some official informations there

@uberspeck
Copy link

I've implemented the solution outlined by @MikeRyanDev with changes suggested by @noelmace and @josh-sachs. Everything looks good and i'm not seeing any console errors etc. However, effects simply refuse to execute. I'm running my effects from my feature module...

/user.module.ts

...
import { UserEffects } from './user.effects';
@NgModule({
  imports: [
    ...,
    EffectsModule.run(UserEffects) // correct??
  ]
})
export class UserModule {
  constructor( private store: Store<AppStateWithUsers> ) {
    store.replaceReducer(appReducerWithUsers);
  }
}

I call my action from my component...

/user-index.component.ts

...
import { UserActions } from './user.actions';
@Component({...})
export class UserIndexComponent implements OnInit {

  constructor(private actions: UserActions) {}

  ngOnInit() {
    this.actions.loadUsers(); // get all the things
  }
}

...the action executes as expected...

/user.actions.ts

@Injectable()
export class UserActions {

  static LOAD_USERS = 'LOAD_USERS';
  public loadUsers(): Action {
    console.log('UserActions.loadUsers()'); // this executes!
    return {
      type: UserActions.LOAD_USERS
    };
  }

  ...
}

...but my effects never execute. What am I missing?

/user.effects.ts

...
import { UserActions } from './user.actions';
@Injectable()
export class UserEffects {

  @Effect() loadUsers$: Observable<Action> = this.actions$
    .ofType(actions.LOAD_USERS)
    .do(a => console.log('action >', a)) // this is NEVER called!
    .switchMap( (action) => ... );

  ...

  constructor(
    private actions$: Actions,
    private actions: UserActions
  ) { }

}

@altschuler
Copy link

@uberspeck You don't ever seem to be dispatching the error in UserIndexComponent. Try injecting the store and do store.dispatch(this.actions.loadUsers()) instead.

@uberspeck
Copy link

ugh. i can't believe i missed that. Thanks @altschuler!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests