Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you compose an AppState with Reducers for multiple resources/components?

Tags:

ngrx

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.

like image 543
Kesarion Avatar asked Jun 12 '17 18:06

Kesarion


People also ask

Can we have multiple reducers in Redux?

Having multiple reducers become an issue later when we create the store for our redux. To manage the multiple reducers we have function called combineReducers in the redux. This basically helps to combine multiple reducers into a single unit and use them.

What is root reducer?

A Redux app really only has one reducer function: the "root reducer" function that you will pass to createStore later on. That one root reducer function is responsible for handling all of the actions that are dispatched, and calculating what the entire new state result should be every time.


1 Answers

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

like image 163
Kesarion Avatar answered Nov 04 '22 16:11

Kesarion