Combining multiple reducers in React.
Break down a single reducer into multiple reducers in React
In this article, we shall discuss combining multiple reducers to create a main reducer. We would then use the main reducer as an argument to useReducer()
hook using react's functional components.
Before we could deep dive into this topic, I assume that you are familiar with how useReducer
hook works and have a brief understanding of combineReducers(reducers)
in Redux. Here's the documentation link if you would want to go through them. useReducer in React | combineReducers in Redux.
The state management functionality of Redux and useReducer
are similar. When the user triggers an action
, this call is dispatched
to the reducer
. The required state
modifications are performed by the reducer, and the latest values are present in the view
. Redux makes use of a store
in addition to how useReducer
works.
While building a smaller project, useContext
+ useReducer
is preferred over Redux
for state management.
As Dan Abramov says "I would like to amend this: don't use Redux until you have problems with vanilla React."
Why do we need multiple reducers?
While creating a react application, it's completely a programmer's decision to either have a single reducer or to break it down into smaller reducers. I went ahead with this approach for the following reasons:
- Taking the example of an eCommerce application, we could have one reducer to manage the user's cart/wishlist, another to contain product details, another to handle the user's address, and so on.
- Dividing reducers based on a given functionality is helpful while writing test cases for each of these features.
- Having smaller reducers increases the readability of code.
- Smaller reducers would mean lesser lines of code for a single function, thus keeping it in line with the clean coding practices.
- We could create smaller reducers and maintain a separate folder structure in this approach.
Implementation with an example application
Let us understand how we could create multiple reducers with an example. We would be using the implementation of an eCommerce application. In this application, we have the following functionalities:
- Set the list of products on page load
- Sort the products based on price
- Add a product to the wishlist
- Remove a product from the wishlist
I have purposely restricted myself with these functionalities and not introduced the cart functionality. We shall discuss this in the last part of the article.
We have a single reducer which is named dataReducer.js
.
// dataReducer.js
import { ActionTypes } from "./actions";
import { toast } from "react-toastify";
export const initialState = {
products: [],
sortBy: "",
wishlist: [],
};
export const reducer = (state, action) => {
switch (action.type) {
case ActionTypes.SET_PRODUCTS: {
return {
...state,
products: action.payload,
};
}
case ActionTypes.SORT_BY_PRICE: {
return {
...state,
sortBy: action.payload.value,
};
}
case ActionTypes.ADD_TO_WISHLIST: {
let updatedList = [...state.wishlist];
const productInWishlist = updatedList.find(
(product) => product.id === action.payload.id
);
if (productInWishlist) {
return state;
}
updatedList = updatedList.concat(action.payload);
toast.success("Book added to wishlist");
return {
...state,
wishlist: updatedList,
};
}
case ActionTypes.REMOVE_FROM_WISHLIST: {
let updatedList = [...state.wishlist];
const productInWishlist = updatedList.find(
(product) => product.id === action.payload.id
);
if (!productInWishlist) {
return state;
}
updatedList = updatedList.filter(
(product) => product.id !== action.payload.id
);
toast.success("Book removed from wishlist");
return {
...state,
wishlist: updatedList,
};
}
default:
return state;
}
};
SET_PRODUCTS
sets the products data which is present withindata/productsData.js
.SORT_BY_PRICE
sets the value to eitherLOW_TO_HIGH
or 'HIGH_TO_LOW`ADD_TO_WISHLIST
adds a product to the wishlist and shows a success toast message.REMOVE_FROM_WISHLIST
removes a product from the wishlist and shows an appropriate toast message.
Decide on how to divide the reducer
From this example, we could easily divide the above-mentioned reducer into 2 - one to handle product-related manipulations and the other to handle the wishlist functionalities.
We are going to do just that ๐
Let us create a new file within the reducers
file named productReducer.js
. This file would contain the initial state for products as well as the reducer that contains the product manipulations.
import { ActionTypes } from "./actions";
export const productState = {
products: [],
sortBy: "",
// could have other properties related to products.
};
export const productsReducer = (state = productState, action) => {
switch (action.type) {
case ActionTypes.SET_PRODUCTS: {
return {
...state,
products: action.payload,
};
}
case ActionTypes.SORT_BY_PRICE: {
return {
...state,
sortBy: action.payload.value,
};
}
default:
return state;
}
};
Similarly, we create another reducer file named wishlistReducer.js
.
import { ActionTypes } from "./actions";
import { toast } from "react-toastify";
export const wishlistState = [];
export const wishlistReducer = (state = wishlistState, action) => {
switch (action.type) {
case ActionTypes.ADD_TO_WISHLIST: {
let updatedList = [...state];
const productInWishlist = updatedList.find(
(product) => product.id === action.payload.id
);
if (productInWishlist) {
return [...state];
}
updatedList = updatedList.concat(action.payload);
toast.success("Book added to wishlist");
return [...updatedList];
}
case ActionTypes.REMOVE_FROM_WISHLIST: {
let updatedList = [...state];
const productInWishlist = updatedList.find(
(product) => product.id === action.payload.id
);
if (!productInWishlist) {
return [...state];
}
updatedList = updatedList.filter(
(product) => product.id !== action.payload.id
);
toast.success("Book removed from wishlist");
return [...updatedList];
}
default:
return state;
}
};
We would now modify dataReducer.js
as follows:
import { productsReducer, productState } from "./productReducer";
import { wishlistReducer, wishlistState } from "./wishlistReducer";
// add all the initialStates to create a single state.
export const initialState = {
...productState,
wishlist: wishlistState,
};
// combine all the reducers to this updated state
export const reducer = (state, action) => {
state = {
...state,
...productsReducer(state.products, action),
wishlist: wishlistReducer(state.wishlist, action),
};
switch (action.type) {
// switch case to check some common state manipulations - if any
default:
return state;
}
};
By modifying the dataReducer.js
as mentioned above, we are good to go! ๐ Yes, we need not make any kind of changes to the other parts of the code. All the hooks and state management would work exactly the same.
Understanding the working
Let's break down and see what happens within dataReducer.js
and understand how this magic works.๐ช
In the first example, our initial state had 3 properties and looked like โฌ๏ธ
export const initialState = {
products: [],
sortBy: "",
wishlist: [],
};
We divided them into 2 states as productState
and wishlistState
export const productState = {
products: [],
sortBy: "",
};
export const wishlistState = [];
Note: Observe how I have used an object
for productState
and an array
in the case of wishlistState
. We could modify the individual states as per our needs while dividing the reducer functionalities.
Now, within dataReducer.js
, we brought back the same intialState
as follows:
export const initialState = {
...productState,
wishlist: wishlistState,
};
We are destructuring productState
to get the properties products
and sortBy
into the same state object as that of wishlist
.
In the same way, we modify the state
within reducer
to obtain the latest modified value from productReducer
and wishlistReducer
respectively.
state = {
...productsReducer(state.products, action),
wishlist: wishlistReducer(state.wishlist, action),
};
We could then add a switch case to this reducer
if there are any further state modifications and return the latest state value to the view
.
I hope this article helped in understanding how we could break down a single reducer into smaller ones based on individual functionalities and yet maintain a single state value. If this sounds interesting, you could extend this code and implement the cart functionality with the same approach.
Hint: We could have cartState
and cartReducer
which can be included within initialState
and reducer
respectively.
Tag me on Twitter and let me know if you were able to add this feature.
If this article was helpful, please give this post a like (with your favorite emoji ๐). Let me know your thoughts in the comments.
Reach out to me on Twitter if you have any queries. Happy learning! ๐ป
Peace โ