9
votes

I'm trying to figure out how to combine many resource states into various component states and what constitutes an AppState. Most ngrx guides/examples out there only handle a resource (e.g. a book) or a limited state (e.g. books and a selected book), but I don't think I've come across anything more complex than that.

What do you do when you have a dozen resources, with various states (list, item, search terms, menu items, filters, etc.) in multiple components requiring different resources states?

I've searched around and I came up with the following structure, but I'm not convinced this is what was intended:

AppState & reducer
<- combine reducers
- Component states & reducers
<- combine reducers
-- Resource states & reducers

You would combine resource reducers (e.g. bookReducer, booksReducer, bookSearchTitleReducer) into a reducer relevant to a component (e.g. bookSearchReducer) and then combine all component reducers into one reducer with one AppState and use the store provider with it in your AppModule.

Is this the way to go or is there another (proper) way to do it? And if this is a good way to do it, would I use Store or Store in a Component constructor?

[Edit]

Ok, the ngrx-example-app does handle more components, I see it only creates states at the component level, not at the resource level, combines the states and respective reducers and uses the full state object in the component constructor: 'store: Store'.

I suppose since it's an official example, this would be the intended way to handle state/reducers.

1

1 Answers

10
votes

[Edit]

The new v4 ngrx is much simpler to use, has a better documentation and example app to help you out. The following is mostly relevant for v2 and its quirks, which are no longer an issue in v4.

[Obsolete]

After a lot of trial and error, I found a good, working formula. I'm going to share the gist of it here, maybe it'll help someone.

The guide on reducer composition helped me a lot and convinced me to go for my original state/reducer structure of Resource > Component > App. The guide is too large to fit here, and you will likely want the up to date version here.

Here's a quick run down of what I had to do in some key files for an app with two components, with two basic resources (user and asset) with derivatives (lists) and parameters (search).

store/reducers/user/index.ts:

import { ActionReducer, combineReducers } from '@ngrx/store';

import { authenticatedUserReducer } from './authenticatedUser.reducer';
import { selectedUserReducer } from './selectedUser.reducer';
import { userListReducer } from './userList.reducer';
import { userSearchReducer } from './userSearch.reducer';
import { User } from '../../models';

const reducers = {
  authenticated: authenticatedUserReducer,
  selected: selectedUserReducer,
  list: userListReducer,
  search: userSearchReducer
};

interface UserState {
  authenticated: User,
  selected: User,
  list: User[],
  search: string
}

const reducer: ActionReducer<UserState> = combineReducers(reducers);

function userReducer(state: any, action: any) {
  return reducer(state, action);
}

export { userReducer, UserState };

store/reducers/asset/index.ts:

import { ActionReducer, combineReducers } from '@ngrx/store';

import { selectedAssetReducer } from './selectedAsset.reducer';
import { assetListReducer } from './assetList.reducer';
import { assetSearchReducer } from './assetSearch.reducer';
import { Asset } from '../../models';

const reducers = {
  selected: selectedAssetReducer,
  list: assetListReducer,
  search: assetSearchReducer
};

interface AssetState {
  selected: Asset,
  list: Asset[],
  search: string
}

const reducer: ActionReducer<AssetState> = combineReducers(reducers);

function assetReducer(state: any, action: any) {
  return reducer(state, action);
}

export { assetReducer, AssetState };

store/reducers/index.ts:

import { routerReducer, RouterState } from '@ngrx/router-store';

import { userReducer, UserState } from './user';
import { assetReducer, AssetState } from './asset';

const reducers = {
  router: routerReducer,
  user: userReducer,
  asset: assetReducer
};

interface AppState {
  router: RouterState,
  user: UserState,
  asset: AssetState
}

export { reducers, AppState };

Note: I included the router reducer supplied separately as well.

app.module.ts:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { RouterModule } from '@angular/router';

import { StoreModule } from '@ngrx/store';
import { RouterStoreModule } from '@ngrx/router-store';

import { reducers } from './store';
import { AppComponent } from './app.component';
import { AppRoutes } from './app.routes';
import { HomeComponent } from './components/home/home.component';

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    RouterModule.forRoot(AppRoutes),
    StoreModule.provideStore(reducers),
    RouterStoreModule.connectRouter()
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Note: Use what you need here, scrap what you don't. I made another index.ts file inside /store, it exports reducers, all the models and perhaps some other things in the future.

home.component.ts:

import { Component } from '@angular/core';

import { Observable } from 'rxjs/Observable';
import { Store } from '@ngrx/store';

import { AppState, User } from '../../store';

@Component({
  selector: 'home',
  templateUrl: './home.template.html'
})
export class HomeComponent {
  user: Observable<User>;

  constructor (private store: Store<AppState>) {
    this.user = store.select('user', 'selected');
    store.dispatch({ type: 'SET_USER_NAME', payload: 'Jesse' });
    store.dispatch({ type: 'ADD_USER_ROLE', payload: 'scientist' });
    store.dispatch({ type: 'ADD_USER_ROLE', payload: 'wordsmith' });
  }
}

Note: You can test with something like {{(user | async)?.name}} in your template.

And that's about it. There may be better ways to do it, I know I could have made it with just one level for example (e.g. just the basic resources), it's all according to what you think fits best for your app.