How to Replicate Reducer Logic into Multiple IDs

Paulo Levy
JavaScript in Plain English
5 min readApr 19, 2021

--

Redux is a powerful application state manager widely used alongside React. Once you define a Reducer, it's possible that you wish to replicate the exact same logic of this single file into multiple identifiers.

TL;DR Complete code Gist || CodeBox Example

What will be achieved?

Let’s suppose a 'Car' reducer that holds and controls the structure below:

{
name: 'Car Reducer Example',
maxSpeed: 150,
famousPilots: []
}

What you'll achieve is the following:

byId:{
111: {
name: 'Car Reducer Example 2',
maxSpeed: 150,
famousPilots: []
},
1040: {
name: 'Car Reducer Example 2',
maxSpeed: 160,
famousPilots: []
},
}

The whole reducer logic (handling actions, updating state) will be replicated to every single ID individually. You define the reducer structure once and reuse it as required.

Data is not only the back-end's responsibility. The front-end should wisely structure its data flow focused on delivering a seamless experience to the user.

Here's an example. Suppose you wish to display a screen with information about the most active users in an application. Your reducer is responsible for coping with data from such screen:

{
mostActiveUser: 'User X Name',
totalNumberOfPosts: 77,
...otherProperties
}

The structure solves the requirements of displaying the required information onscreen. However, it has limited capabilities as it is only capable of holding data for a specific period.

If the shown information is related to the month of April, in order to change the user visualization to May, you would overwrite the whole data, which is a waste of resources and time once the user decides to check April again.
Not only would you make the user wait for some data they already had previously, but you are also making a repeated request to the server.

Applying the handler for multiple IDs considering the month as the identifier you can achieve the following:

byId:{
'2020-04': {
mostActiveUser: 'User X Name',
totalNumberOfPosts: 77,
...otherProperties
},
'2020-05': {
mostActiveUser: 'User Y Name',
totalNumberOfPosts: 53,
...otherProperties
},
}

The reducer stays the same, its implementation does not change. However, you enhance it by making it able of reproducing every single change it's responsible for but handling those changes in a specific month.

Live Example

In the example below, a Reducer is designed to hold data for a single Pokémon. To replicate its logic, the Multiple IDs Handler is applied and the Pokémon name is used as an identifier.

Notice that the reducer structure holds data for a single pokémon.
Using the handler its logic is replicated to multiple pokémons.

What is the motivation for applying such functionality?

The examples above show rather simple cases, with simple structures. On a real-world application, it's more likely reducers will control more data and be responsible for handling complex actions.

Once this structure is defined, you should avoid replicating it by simply copying code from one reducer to another. We'll go through the process of building the handler to replicate the entire reducer functionality to work on multiple IDs.

This is powerful if you wish to apply the whole reducer logic on multiple dynamic items, identifiers not yet available prior to fetching some data, as with the pokémon names in the live example.

It's also powerful to avoid wasting resources, by not making repeated requests, which also plays an important role in the User Experience as you won't make the user wait for information he already possess.

Getting started

The first thing to notice when building such a handler is that reducers are simply functions: they expect two arguments (state and action) and return an updated state based on the passed action.

// Reducer function example
function myReducer(state,action) => {
switch(action.type){
default: return state
}
}

This means we're allowed to apply a function on top of it and make modifications to the final state that will be used. This is the concept of a Higher-Order Reducer.

Creating the Multiple IDs Reducer Handler

Create a new file and insert the following code:

export const multipleIdsReducerHandler = (reducerFunction) => (state = {}, action) => {
const { internalReducerId } = action
if(internalReducerId === undefined) return state
}

So far it does a simple job, it'll receive a reducer function (the one we wish to expand its functionality into multiple IDs) and then the state and action as any reducer does. It will automatically return the current state if the property internalReducerId is not present in the action.

At this point, a structure to identify on what specific ID the reducer logic should be applied to is defined: internalReducerId must exist on the action so that the handler can identify on what object (ID) to apply the result of the execution of your desired reducer.

The following code is responsible for executing the desired reducer and obtaining the resulting state from the action call. Also, the comparison on the bottom avoids including a new ID on a reducer that is not the desired one.

    const newState = reducerFunction(state.byId?.[internalReducerId] || {}, action)    if(Object.keys(newState).length === 0 ) return state

Picture the following scenario where multipleIdsReducerHandler is applied to two reducers: If we are setting an ID on Reducer1, it shouldn’t be set on Reducer2 as well. Since all reducers functions are run when an action is dispatched, we would fall into the ‘default’ case of the second reducer (as it doesn’t implement the dispatched action). But at this point, we can’t proceed with our manipulation, otherwise, we would add the ID property (defined in the action as internalReducerId) into all reducers that implement the handler, this is why we return the state automatically when this attempt is identified.

Now what's left is taking the result from the execution of the desired reducer and setting it to the specified ID, and returning the whole state.

return {
...state,
byId:{
...state.byId,
[internalReducerId]: { ...newState }
}
}

The handler is complete. Simple, yet powerful. You can check this gist for the full code.

To apply it, import multipleIdsReducerHandler to your desired Reducer file and wrap its export with it:

export default multipleIdsReducerHandler(myReducerFunction)

And when dispatching an action related to the desired reducer, always include the internalReducerId property:

dispatch({ ...otherProperties, internalReducerId: myId })

Complete code for the Multiple IDs Reducer Handler

You can also check this CodeSandbox from the Pokémon example to check implementation details.

Bonus — Typed Dispatch

If you are working with TypeScript, it would be a great idea to have the dispatch function typed in order to enforce the requirement of internalReducerId property when dispatching an action.

export type DispatchWithMultipleIds = (
args: {
[x: string]:any;
internalReducerId: string;
}
) => void;

Simply import the type and use it on the dispatch argument. For example:

return async (dispatch: DispatchWithMultipleIds, getState) => ...

You can learn more about Higher-Order Reducers and Reusing Reducer logic on the official Redux docs.

More content at plainenglish.io

--

--