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

How do I use ngrx with generic effects, actions, and reducers? #404

Closed
danielmhair opened this issue May 2, 2017 · 2 comments
Closed

How do I use ngrx with generic effects, actions, and reducers? #404

danielmhair opened this issue May 2, 2017 · 2 comments

Comments

@danielmhair
Copy link

danielmhair commented May 2, 2017

I have been reviewing a lot of videos and documentations to better understand ngrx. I am trying to use ngrx properly, which is the purpose of this question. I love the power you get with state separation and my use case definitely applies to this method.

This is my use case
My team uses Kendo's Grid to display almost all of our data. Because we use this grid throughout our app, we created a wrapper around it, to give it styling and created components that helped make it easier for the developer to use Kendo without worrying about styling. Before we knew about ngrx, we had a few services that handled selections, our data (from an API) and other parts of our state. The part that makes this complicated is that we have multiple types of grids that each have their own set of data, as well as their own grid. For example, we may have a list of properties that shows a grid, another grid that has a list of projects. So each will use the grid, but will work with different types of data. Our main goal is to limit the amount of code that the consuming developer has to do. Because of this constraint, each grid will need their own set of state, map to their own set of actions and reducers. This is the main reason I am struggling to understand how best to use ngrx.

The "best" way that I have found is to create a class that acts like a Factory, where it asks for the url and name of your store, then it will return the reducers, as well as the effects you need to get that grid working properly.

export class ApiModel {
  public id: string
  constructor(id: string) {
    this.id = id
  }
}

export class GridReducerAndEffects<T extends ApiModel> {
  public actionReducer: ActionReducer<any>
  public PrmApiEffects: Type<PrmApiEffects<T>>
  public PrmSlideOutEffects: Type<PrmSlideOutEffects>
  public GridEffects: Type<PrmGridEffects<T>>
  public PrmGridSelectionEffects: Type<PrmGridSelectionEffects<T>>

  constructor(public storeName: string, public baseUrl: string) {
    this.GridEffects = class GridEffects extends PrmGridEffects<T> {
      public storeName: string = storeName
      public baseUrl: string = baseUrl
    }

    this.SlideOutEffects = class SlideOutEffects extends PrmSlideOutEffects {
      public storeName: string = storeName
    }

    this.ApiEffects = class ApiEffects extends PrmApiEffects<T> {
      public storeName: string = storeName
      public baseUrl: string = baseUrl
    }

    this.GridSelectionEffects = class GridSelectionEffects extends PrmGridSelectionEffects<T> {
      public storeName: string = storeName
    }

    const actionReducers = {}
    actionReducers[SLIDE_OUT_STATE_KEY(this.storeName)] = SlideOutActionReducer(this.storeName)
    actionReducers[SELECTION_STATE_KEY(this.storeName)] = GridSelectionActionReducer(this.storeName)
    actionReducers[API_STATE_KEY(this.storeName)] = ApiActionReducer<T>(this.storeName)

    this.actionReducer = combineReducers(actionReducers)
  }
}

To show how one of the effects class, that is generic, work; here is the PrmApiEffects:

export abstract class PrmApiEffects<T extends PrmApiModel> {
  public abstract storeName: string
  public abstract baseUrl: string

  @Effect()
  public prmApiGet: Observable<Action> = this.actions
      .ofType(PRM_API_GET_ACTION(this.storeName))
      .map((action: PrmApiGetAction) => action.payload)
      .switchMap((params: PrmApiGetParams) => prmApiGetRequest<T>(this.baseUrl, params)) // <= imported function that does `this.http.get(...).map(res => res.json())` 
      .map((results: PrmApiState<T>) => new PrmApiGetSuccessAction<T>(this.storeName, results))
      .catch((err) => Observable.of(new PrmApiGetFailureAction(this.storeName, err)))
  ...
  constructor(@Inject(Actions) private actions: Actions,
                     @Inject(Http) private http: Http) {}
}

To show how the SlideOutActionReducer is made, here is the function:

export function GridSlideOutActionReducer(storeName: string): ActionReducer<SlideOutState> {
  return (state: SlideOutState, action: SlideOutActions): SlideOutState => {
    switch (action.type) {
    case PRM_SLIDE_OUT_BUTTON_SWITCH_ACTION(storeName):
      state.prmSlideOutComponent = action.payload
      return state
    case PRM_SLIDE_OUT_SWITCH_COMPONENT_ACTION(storeName):
      state.PrmSlideOutButtons = action.payload
      return state
    default:
      return state
    }
  }
}

Then, for our Property Grid, we would instantiate this class:

export const PROPERTY_STORE_NAME = "Property"
export const PROPERTY_BASE_URL = "www.example.com/api/properties"
export const PropertyReducerAndEffects = new GridReducerAndEffects(PROPERTY_STORE_NAME, PROPERTY_BASE_URL)

Same goes for the project grid

export const PROJECT_STORE_NAME = "Property"
export const PROJECT_BASE_URL = "www.example.com/api/properties"
export const ProjectReducerAndEffects = new GridReducerAndEffects(PROJECT_STORE_NAME, PROJECT_BASE_URL)

In our module, we would then do this:

...
  imports: [
    AppRoutingModule,
    CoreModule,
    TranslateModule.forRoot({
      provide: TranslateLoader,
      useClass: AppTranslateLoader,
    }),
    // Project Reducers and Effects
    StoreModule.provideStore(ProjectReducerAndEffects.actionReducer),
    EffectsModule.run(ProjectReducerAndEffects.GridEffects),
    EffectsModule.run(ProjectReducerAndEffects.ApiEffects),
    EffectsModule.run(ProjectReducerAndEffects.GridSelectionEffects),
    EffectsModule.run(ProjectReducerAndEffects.SlideOutEffects),
	
    // Property Reducers and Effects
    StoreModule.provideStore(PropertyReducerAndEffects.actionReducer),
    EffectsModule.run(PropertyReducerAndEffects.GridEffects),
    EffectsModule.run(PropertyReducerAndEffects.ApiEffects),
    EffectsModule.run(PropertyReducerAndEffects.GridSelectionEffects),
    EffectsModule.run(PropertyReducerAndEffects.SlideOutEffects),
  ],
...

Critique of this method
Obviously, having a factory class that generates classes that act as services for angular is very odd and quite unusual. Honestly, I almost feel as though this is the way we will need to do it, so we have specific store names for each type of grid (whether it be for projects or properties, etc).

I understand that I didn't show all the other files that I have implemented to make this work. I did that for a reason. I am asking more for guidance about this approach rather than solving the errors I'm dealing with. If your guidance shows that this is the right way, then I will talk about the errors that I am getting in more depth.

Also, to see how this comes together with specific store names, here is an example of a reducer:

export function selectData<T extends ApiModel>(storeName: string) {
  return (state: ApiState<any>): any[] => {
    return state[API_STATE_KEY(storeName)].data
  }
}

And here is an example of our action:

export function GRID_ACTIONS(storeName: string) {
  return `Grid${storeName ? " " + storeName : ""}`
}

export function CREATE_PROPERTY_ACTION(storeName: string) {
  return `[${GRID_ACTIONS(storeName)}] Get Page Failure`
}

export class CreatePropertyAction implements Action {
  public readonly type
  constructor(storeName: string, public payload: Type<{}>) {
    this.type = CREATE_PROPERTY_ACTION(storeName)
  }
}

This way, in our component, we simply do this:

@Component({...})
export class PropertyGridComponent {
    properties: Observable<Property[]>
    public storeName: string = "Property"

    constructor(...) {
       this.properties = this.store.select(selectData<Property>(this.storeName))
    }

    dispatchNewProperty(newProperty: Property) {
       this.store.dispatch(new CreatePropertyAction(this.storeName, newProperty))
    }
}

In this way, each reducer and action is specific to the store name, but still limits how much the developers need to do when they make their specified grid.

Any suggestions or ideas?

@danielmhair
Copy link
Author

This problem was easily resolved by passing in the store name and store url into the payload where it was needed. This way, there was no need to have this class that created Effect classes.

@eharkins
Copy link

eharkins commented Aug 11, 2017

I've been trying to get a very similar solution working. This seems to be one of the more elegant ways of generalizing the actions, effects, reducers of ngrx/store. Did you end up getting this to work with the store url and name in the action payload in order to not generate the effects classes for each of your types in the store? If so, could you share that solution as I think it could apply to my own use case. Thanks!

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

2 participants