De-duplicating logic of reduction action with high-order reducers

I am refactoring a React / Redux application that has been built to the point where there is a need to duplicate a lot of reducer logic. I have looked at Red Order Reducers but I am having a hard time getting my head around how I can get them to work for me.

Here's an example:

reducers.js

import components from './entities/components';
import events from './entities/events';
import items from './entities/items';

const entities = combineReducers({
  components,
  items,
  events,
});

      

I am using normalizr. Here's an idea of โ€‹โ€‹what my state looks like. For this example, just be aware that events can belong to components or elements, but they can be different.

{
  entities: {
    components: {
      component_1: {
        events: ['event_1'],
      },
    },
    items: {
      item_1: {
        events: ['event_2'],
      },
    },
    events: {
      event_1: {
        comment:  "foo"
      },
      event_2: {
        comment:  "bar"
      }
    }
  }
}

      

I use action type EVENT_ADD

in all 3 reducers. The logic is identical for component.js and item.js (see below), but they also have independent actions specific to their type.

Here I got around unnecessarily adding an event in the wrong place by using the Immutable.js function .has()

to check if the target id exists in that part of the state.

export default (state = getInitState('component'), action) => {
  switch (action.type) {
    case EVENT_ADD:
      return state.has(action.targetId) ?
        state.updateIn(
          [action.targetId, 'events'],
          arr => arr.push(action.event.id)
        ) :
        state;

    [...]
  }
}

      

In event.js I am using here EVENT_ADD

to add event content to this part of the state.

export default (state = getInitState('events'), action) => {
  switch (action.type) {
    case EVENT_ADD:
      return state.set(action.event.id, fromJS(action.event));

    [...]
  }
}

      

My understanding of high order reducers is that I need to do one or both of the following:

  • Use specific types of action ADD_EVENT_${entity}

    (such as to use the event EVENT_ADD_COMPONENT

    and EVENT_ADD_ITEM

    in event.js to create an event, regardless of its parent.)
  • Filter by name in my main reducer as shown at the bottom of the document , in which case I'm not sure how to distinguish between my initial state and actions that are component and element specific if I just use a reducer for both.

I think I need sharedReducer.js where I can share logic between components and elements for actions like ADD_EVENT

, DELETE_EVET

etc. And then there are also two independent gearboxes for each organization. I don't know how I can do this or which design pattern is best.

Any guidance would be greatly appreciated. Thank!

+3


source to share


2 answers


The phrase "higher order $ THING" usually means "a function that takes $ THING as an argument and / or returns a new $ THING" (where "$ THING" is usually a "function", "component", "reducer", etc. etc.). Thus, a "higher order reducer" is a function that takes some arguments and returns a reducer.

In the section on Reducing Reducers - Reusing Reducer Logic that you link, the most common example of a higher order reducer is a function that takes some kind of distinctive value, such as an identifier or an action type or substring, and returns a new reducer function , which uses this distinction to know when it should actually respond. This way, you can create multiple instances of the same function, and only the "right" one will respond to the given action, even if the rest of the logic is the same.

It seems that you are usually on the right track already, and there are several ways to organize the overall logic.

One approach might be to explicitly look for "generic" actions first and run that reducer and then look for specific cases:



export default (state = getInitState('events'), action) => {
    // First check for common cases
    switch(action.type) {
        case EVENT_ADD:
        case OTHER_COMMON_ACTION: {
            state = sharedReducer(state, action);
            break;
        }        
    }

    switch(action.type) {
        case EVENT_ADD:
        case EVENTS_SPECIFIC_ACTION: {
            return eventsSpecificReducer(state, action);
        }
    }

    return state;
}

      

Another approach could be to use something like reduce-reducers

, as shown in Structuring Reducers - OutsidecombineReducers

section:

const entities = combineReducers({
    components : reduceReducers(componentsReducer, sharedReducer),
    items : reduceReducers(itemsReducer, sharedReducer)
    events,
});

      

Hopefully this will give you some pointers in the right direction. If you have further questions feel free to ask here, or in the Reactiflux chat channels on Discord - invite a link to https://www.reactiflux.com . (Note: I am a Redux developer, author of this "Structuring Reducers" section and admin Reactiflux.)

+4


source


I had a similar problem wrapping my head around higher order reducers. Even with experience writing higher-order functions and components, there are many limitations with reducers that make things difficult.

I came up with a solution like this:



export default function withSomething(reducer) {
    const initialState = {
        something: null,
        ...reducer(undefined, {}),
    };

    return function (state = initialState, action) {
        let newState = state;

        switch (action.type) {
            case actionTypes.SET_SOMETHING:
                newState = {
                    ...state,
                    something: action.something,
                };
                break;
            default:
        }

        return reducer(newState, action);
    };
}

      

The basic "tricks" call the undefined wrapper reducer to get its initial state and add properties to it, and also pass the new state that results from handling HOR actions on the wrapped reducer. You can optimize without naming the wrapped reducer if newState! = State after the switch construct, but I feel this way is more flexible as it potentially allows you to handle the wrapped reducer handler and action that has already been handled and do something with his own part of the state.

0


source







All Articles