Reputation: 359
I'm refactoring a React/Redux app that has been built to a point where there is a need to de-duplicate a lot of reducer logic. I've looked into High Order Reducers but I'm having a hard time wrapping 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'm using normalizr. Here's an idea of what my state looks like. For this example, just know that events can belong to components or items, but the two are otherwise distinguishable.
{
entities: {
components: {
component_1: {
events: ['event_1'],
},
},
items: {
item_1: {
events: ['event_2'],
},
},
events: {
event_1: {
comment: "foo"
},
event_2: {
comment: "bar"
}
}
}
}
I'm using the EVENT_ADD
action type in all 3 reducers. The logic is identical for component.js and item.js (below), but the two also have independent actions that are specific to their type.
Here, I got around unnecessarily adding the event to the wrong place by using Immutable.js's .has()
function 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 use EVENT_ADD
here as well to add the contents of an event to that portion 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:
ADD_EVENT_${entity}
(thus making me use both EVENT_ADD_COMPONENT
and EVENT_ADD_ITEM
in event.js to create an event regardless of its parent.)It seems like I want some sharedReducer.js where I can share logic between components and items for actions like ADD_EVENT
, DELETE_EVET
, etc. And then also have two independent reducers for each entity. I'm unsure how I can accomplish this or what design pattern is best.
Any guidance would be appreciated. Thank you!
Upvotes: 4
Views: 1574
Reputation: 67499
The phrase "higher-order $THING" generally means "a function that takes a $THING as an argument, and/or returns a new $THING" (where "$THING" is usually "function", "component", "reducer", etc). So, a "higher-order reducer" is a function that takes some arguments, and returns a reducer.
Per the Structuring Reducers - Reusing Reducer Logic section you linked, the most common higher-order reducer example is a function that takes some kind of distinguishing value like an ID or action type or substring, and returns a new reducer function that uses that distinguishing value to know when it's actually supposed to respond. That way, you can create multiple instances of the same function, and only the "right" one will respond to a given action even though the rest of the logic is the same.
It seems like you're generally on the right track already, and there's a few ways you could possibly organize the shared logic.
One approach might be to explicitly look for the "common" actions first and run that reducer, and then look for the 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 might be to use something like the reduce-reducers
utility, as shown in the Structuring Reducers - Beyond combineReducers
section:
const entities = combineReducers({
components : reduceReducers(componentsReducer, sharedReducer),
items : reduceReducers(itemsReducer, sharedReducer)
events,
});
Hopefully that gives you some pointers in the right direction. If you've got more questions, feel free to ask here, or in the Reactiflux chat channels on Discord - invite link is at https://www.reactiflux.com. (Note: I'm a Redux maintainer, author of that "Structuring Reducers" docs section, and an admin on Reactiflux.)
Upvotes: 5
Reputation: 2279
I've had a similar problem wrapping my head around higher order reducers. Even with experience writing higher order functions and components, there's a lot of constraints with reducers that make it more 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 main "tricks" are calling wrapper reducer with undefined state to get its initial state and add properties to it, and passing the new state that results from HOR's action handling to the wrapped reducer. You could optimize by not calling the wrapped reducer if newState != state after the switch construct, but I feel this way is more flexible because it potentially lets the wrapped reducer handle and action that has already been handled and do something with its own part of the state.
Upvotes: 0