Combining multiple reducers in React.
Tags: webdev, beginners, react, reduce
Published: 2022-03-18 15:46:12

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](https://reactjs.org/docs/hooks-reference.html#usereducer) | [combineReducers in Redux](https://redux.js.org/api/combinereducers).

The state management functionality of [Redux](https://redux.js.org/) 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.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6xampyriz1a1aniea9ei.png)


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.

[Link to CodeSandBox](https://codesandbox.io/s/usereducer-demo-1-e1qehp)

We have a single reducer which is named `dataReducer.js`.

```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 within `data/productsData.js`.
- `SORT_BY_PRICE` sets the value to either `LOW_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.

[Link to CodeSandBox](https://codesandbox.io/s/usereducer-demo-2-6l1ply)

```js
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`.

```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:

```js
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 ⬇️

```js
export const initialState = {
  products: [],
  sortBy: "",
  wishlist: [],
};
```

We divided them into 2 states as `productState` and `wishlistState`

```js
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:

```js
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.

```js
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](https://codesandbox.io/s/usereducer-demo-2-6l1ply) 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](https://twitter.com/supminn) 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 ✌