-
Notifications
You must be signed in to change notification settings - Fork 311
Modules, lazy-loading and multiple stores? #197
Comments
@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 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 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 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 import { AppStateWithBlog, appReducerWithBlog } from '../blog/reducer';
@Component({
selector: 'blog-page',
template: `...`
})
export class BlogPageComponent {
constructor(private store: Store<AppStateWithBlog>) {
store.replaceReducer(appReducerWithBlog);
}
} Now 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! |
Thank you very much for comprehensive answer, really appreciated! |
@MikeRyan52 we should create a FAQ and include this there.. it keeps coming up on gitter. Daily. |
+1 :) |
How can you prevent to cleanup of the blog state reducer? |
@MikeRyan52 's answer should be in the docs somewhere 💯 |
@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? |
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). |
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...
I had to use MikeRyan52's defining appReducer as a constant of the createReducer() output. |
@noelmace How are you using heroesStoreFactory in your example? I was trying something like in my feature module:
|
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...
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:
|
@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
}; |
Why don't you all just wait couple of days / weeks for v4 to be released on npm? |
@fxck Are there any official informations about the next release? |
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. |
watch this talk https://www.ng-conf.org/sessions/inactive-reactive-ngrx/ and you might get some official informations there |
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...
...
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...
...
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...
@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?
...
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
) { }
} |
@uberspeck You don't ever seem to be dispatching the error in |
ugh. i can't believe i missed that. Thanks @altschuler! |
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?
The text was updated successfully, but these errors were encountered: