Reusing reducers logic

693 Views Asked by At

Problem

I'd like to chain multiple high order reducers using ngrx, so that similar parts of code in my reducers have only one implementation.

My application has number of pages that have quite similar functionalities. Their reducers also look quite similar. For this example let's consider an application with three pages: Page One, Page Two and Page Three. Each of these pages contains a counter, but is allowed to do different things with it. And so:

  • Page One can increment and decrement value of counter
  • Page Two can increment, decrement and reset value of counter
  • Page Three can increment and reset value of counter

Example of such application can be found here. It is very naive implementation with separate reducer for each page - a lot of very similar functionalities are repeated in each reducer.

Solution with only one high order reducer

I managed to move part of common logic to separate piece of code using high order reducer. In this case it is increment functionality:

interface Actions {
  incrementAction: ActionCreator<string>;
}

export const withIncrementation = ({ incrementAction }: Actions) => (
  initState,
  ...actions
) =>
  createReducer(
    initState,
    on(incrementAction, state => ({
      ...state,
      counter: state.counter + 1
    })),
    ...actions
  );

This high order reducer can be used like this:

const pageOneReducer = withIncrementation({
  incrementAction: PageOneActions.IncrementRequested
})(
  initialState,
  on(PageOneActions.DecrementRequested, (state: State) => ({
    ...state,
    counter: state.counter - 1
  }))
);

export function reducer(state: State | undefined, action: Action) {
  return pageOneReducer(state, action);
}

Up to this point everything works fine. Application that works correctly like this is here

Problem with making multiple high order reducers work

Problem starts, when I try to chain multiple high order reducers to work. In example application both Page Two and Page Three are capable of resetting counter (and incrementing too), so I'd like to use 2 high order reducers now. I prepared new high order reducer, very similar to the previous one, that should do the work:

export const withReseting = ({ resetAction }: Actions) => (
  initState,
  ...actions
) =>
  createReducer(
    initState,
    on(resetAction, state => ({
      ...state,
      counter: initialState.counter
    })),
    ...actions
  );

It works fine when it's alone, however I can't find a way to make it work in such way, that both withIncrementation and withReseting work fine on the same reducer. Example application where I tried to achieve that can be found here, but it does not work (it seems like my state stops working at all).

Naive approach with 3 separate reducers

Single high order reducer working fine

Chaining high order reducers that do not work

1

There are 1 best solutions below

0
Nick S On

I came across your question because I was trying to implement something similar within my application. I am accessing a lot of entities from my backend API, and all are similar and require similar reducers.

Your post was helpful, as it showed me an approach that would not work: that is, chaining the reducers. That got me thinking: what if we stick with a single reducer, but generated the ons() with a higher-order function?

Helper Type

First, I created a little Action helper type to make it easier to configure Actions with parameters:
export type ActionWithParams<T> = ActionCreator<string, (props: T) => T & TypedAction<string>>;

Higher-Order Function

Then I created my higher-order function. For this example, we will generate three Action Reducers, two of which will have parameters. Your functions can contain as many or as few Action Reducers as you need. Note that I am using a generic State type, which as you would expect means that all reducers that use this function must share a similar State. Obviously, you could also just hard-code a specific State type here as well.
interface MyActionsConfig {
  action1: ActionWithParams<{param: string}>;
  action2: ActionWithParams<{param: boolean}>;
  action3: ActionCreator<string>;
}

export const createMyReducerOns = <T>(config: MyActionsConfig): ReducerTypes<MyState<T>, ActionCreator[]>[] => {
  return [
    on(config.action1, (state, action): MyState<T> => ({
      ...state, stringProperty: action.param
    })),
    on(config.action2, (state, action): MyState<T> => ({
      ...state, booleanProperty: action.param
    }),
    on(config.action3, (state): MyState<T> => ({
      ...state, someOtherProperty: false
    })
  ];
}

The Reducer

Finally, we come to actually creating the reducer. This ends up looking pretty much like normal (this is assuming you've already defined an initialState). What's cool here is, you can easily mix and match multiple on() generation functions (which I believe matches your initial scenario perfectly), and of course create one-off on()s as usual.

import * as myActions from './my-actions'
export const myReducer = createReducer(
  initialState,
  ...createMyReducerOns<MyType>({
    action1: myActions.action1,
    action2: myActions.action2,
    action3: myActions.action3
  }).
  ...anotherCreateReducerOns<MyType>({
    action1: myActions.differentAction1,
    action2: myActions.differentAction2,
    action3: myActions.differentAction3
  }),
  on(myActions.uniqueAction, (state, action): MyState<T> => {
    ...state, superSpecialField: action.value
  })
);

Note: you can even pass-in an instance of EntityAdapter<MyType> from @ngrx/entity (if you're using this library) via your config interface and use all the adapter() methods as well.