Write Maintainable React Component
Tags: react, javascript, webdev, tutorial
Published: 2022-10-26 13:29:07

I recently came to the conclusion that React [container](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) components do not scale well.  we usually put the business logic in Container components, which eventually grow into very large containers with too many responsibilities. so it's very difficult to maintain, refactor, and work with. 

There are numerous approaches to writing maintainable React components. But for the purposes of this article, I'm leaning toward the **useState -> useReducer -> Domain Hook** path. 

The _useReducer_ hook is an alternative to the _useState_ hook in appropriate situation. Let's see how to component transition from _useState_ to _useReducer_ and then _useReducer_ in the next level with a **custom hook** also how React can help us solve this problem without the use of any additional libraries.

## Domain Component Objectives:

Consider managing a simple inventory system. Here we are more interested in building product list components with minimal functionality such as marking as sold out, adding stock, sorting by title, and rating, and when one of the products is completely sold out, the render stock availability is red badge(Not Available) otherwise it will be green badge (Available).


![React.js useReducer abstraction](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ann7mlrsndgcys89jba4.png)

## With useState:

Here is _ProductList_ component created using [React.useState](https://reactjs.org/docs/hooks-reference.html#usestate)

```javascript
import React from "react";
import { mockProducts, Product } from "../products-mock";

export function ProductList() {
  const [products, setProducts] = React.useState(mockProducts);

  const handleSortBy = (by: "rating" | "title") => {
    let sortedProducts;
    if (by === "rating") {
      sortedProducts = [...products].sort((a, b) => a[by] - b[by]);
    } else {
      sortedProducts = [...products].sort((a, b) => a[by].localeCompare(b[by]));
    }
    setProducts(sortedProducts);
  };

  const isProductsInStock = products.every((p) => p.stock > 0);

  const handleSoldOut = (id: number) => {
    const results = products.map((p) => {
      if (p.id === id) {
        return {
          ...p,
          stock: 0,
        };
      }
      return p;
    });
    setProducts(results);
  };

  const handleAddStock = (id: number, stock: number) => {
    const results = products.map((p) => {
      if (p.id === id) {
        return {
          ...p,
          stock: stock,
        };
      }
      return p;
    });
    setProducts(results);
  };

  return (
    <div className="container">
      
      <pre>
        Product Availability :
        {isProductsInStock ? " Available " : " Not Available "}
      </pre>


      <div className="action-buttons">
        <button onClick={() => handleSortBy("rating")}>sort by rating</button>
        <button onClick={() => handleSortBy("title")}>sort by name</button>
      </div>


      <ul className="product-list">
        {products.map((product) => {
          return (
            <li className="product-list__item">
              <span>
                {product.title} - ( {product.stock} )
              </span>
              <button onClick={() => handleSoldOut(product.id)}>Sold out</button>
              <button
                onClick={() =>
                  handleAddStock(product.id, Math.ceil(Math.random() * 50 + 50))
                }
              >
                Add Stock
              </button>
            </li>
          );
        })}
      </ul>
    </div>
  );
}

```
Keep in mind that all logic is close to the component and reveals how we manipulate the model within the component itself.

**Pros:** 

 - Basic features are simple to implement.

**Cons:** 

- The component became fat as a result of state management within the component.
- Separate of concerns
- Difficult to manage dependent states such as isLoading, success, failure while fetching Product List, and so on.
- With independent state variables, a complex operation like filtering based on other state variables is overly complex.
- Undo/Redo functions

## With useReducer:

The [useReducer](https://reactjs.org/docs/hooks-reference.html#usereducer) hook accepts a reducer type `(state, action) => newState` and returns a state object paired with a dispatch method much like Redux.

```javascript
import React from "react";
import { mockProducts, Product } from "../products-mock";

type ProductListState = {
  products: Product[];
};

enum ProductListActionKind {
  SORT_BY_RATING = "SORT_BY_RATING",
  SORT_BY_NAME = "SORT_BY_NAME",
  PRODUCTS_IN_STOCKS = "PRODUCTS_IN_STOCKS",
  PRODUCT_SOLD_OUT = "PRODUCT_SOLD_OUT",
  ADD_STOCK_PRODUCT = "ADD_STOCK_PRODUCT",
}

type ProductListAction = {
  type: ProductListActionKind;
};

function productListReducer(
  state: ProductListState,
  action: ProductListAction
) {
  switch (action.type) {
    case ProductListActionKind.SORT_BY_RATING: {
      return {
        ...state,
        products: [...state.products].sort((a, b) => a.rating - b.rating),
      };
    }
    case ProductListActionKind.SORT_BY_NAME: {
      return {
        ...state,
        products: [...state.products].sort((a, b) =>
          a.title.localeCompare(b.title)
        ),
      };
    }
    case ProductListActionKind.PRODUCT_SOLD_OUT: {
      return {
        ...state,
        products: state.products.map((p) => {
          if (p.id === action.payload.id) {
            return {
              ...p,
              stock: 0,
            };
          }
          return p;
        }),
      };
    }
    case ProductListActionKind.ADD_STOCK_PRODUCT: {
      return {
        ...state,
        products: state.products.map((p) => {
          if (p.id === action.payload.id) {
            return {
              ...p,
              stock: action.payload.stock,
            };
          }
          return p;
        }),
      };
    }
    default:
      return state;
  }
}

export function ProductList() {
  const [state, dispatch] = React.useReducer(productListReducer, {
    products: mockProducts,
  });

  const isProductsInStock = state.products.every((p) => p.stock > 0);

  return (
    <div className="container">
      <pre>
        Product Availability :
        {isProductsInStock ? " Available " : " Not Available "}
      </pre>

      <div className="action-buttons">
        <button
          onClick={() =>
            dispatch({ type: ProductListActionKind.SORT_BY_RATING })
          }
        >
          sort by rating
        </button>
        <button
          onClick={() => dispatch({ type: ProductListActionKind.SORT_BY_NAME })}
        >
          sort by name
        </button>
      </div>
      <ul className="product-list">
        {state.products.map((product) => {
          return (
            <li className="product-list__item">
              <span>
                {product.title} - ( {product.stock} )
              </span>
              <button
                onClick={() =>
                  dispatch({
                    type: ProductListActionKind.PRODUCT_SOLD_OUT,
                    payload: {
                      id: product.id,
                    },
                  })
                }
              >
                Sold out
              </button>
              <button
                onClick={() =>
                  dispatch({
                    type: ProductListActionKind.ADD_STOCK_PRODUCT,
                    payload: {
                      id: product.id,
                      stock: Math.ceil(Math.random() * 50 + 50),
                    },
                  })
                }
              >
                Add Stock
              </button>
            </li>
          );
        })}
      </ul>
    </div>
  );
}
```

**pros:**

- Isolate state management with reducer functions
- The component appears to be cleaner than before.
- Undo/Redo capability

**cons:**

- There is no way to write a faster component because there is a lot of boilerplate code, such as constants/enum for action type.


## Introduce Custom Hook with abstraction:


**Abstraction:** we extracted all business logic into custom hooks called _useProducts_ along with _useReducer_ hooks, and now the client doesn't have to worry about constant types, importing all the action types inside the component. Also it's not necessary to understand the underlying implementation. It reduces the possibility of errors during dispatch type construction and we can refactor reducer function without worrying too much about the consumer of _useProducts_ hook.

We abstracted **dispatch** method with a nice set of API methods exposed, and we can test the custom hooks separately.

_src/shared/hooks/useProducts.js_

```javascript

export function useProduct() {
  const [{ products }, dispatch] = React.useReducer(productListReducer, {
    products: mockProducts,
  });

  const sortBy = (by: "rating" | "title") => {
    const type =
      by === "rating"
        ? ProductListActionKind.SORT_BY_RATING
        : ProductListActionKind.SORT_BY_NAME;
    dispatch({ type });
  };

  const isProductsInStock = () => products.every((p) => p.stock > 0);
  const setSoldOut = (id: number) =>
    dispatch({ type: ProductListActionKind.PRODUCT_SOLD_OUT, payload: { id } });

  const addStock = (id: number, stock: number) =>
    dispatch({
      type: ProductListActionKind.ADD_STOCK_PRODUCT,
      payload: { id, stock },
    });

  return {
    products,
    sortBy,
    isProductsInStock,
    setSoldOut,
    addStock,
  };
}

```

**Encapsulation**: Much cleaner than before, capable of managing complex state through useReducer and abstracting useReducer's functions like _dispatch_. In addition, I encapsulated all of the behaviour under a custom hook with useful methods like _sortBy, setSoldOut, and addStock_.

```javascript
import React from "react";
import { useProduct } from "../hooks/useProduct";

export function ProductList() {
  const { products, sortBy, isProductsInStock, setSoldOut, addStock } =
    useProduct();

  return (
    <div className="container">
      <pre>
        Product Availability :
        {isProductsInStock() ? " Available " : " Not Available "}
      </pre>
      <div className="action-buttons">
        <button onClick={() => sortBy("rating")}>sort by rating</button>
        <button onClick={() => sortBy("title")}>sort by name</button>
      </div>
      <ul className="product-list">
        {products.map((product) => {
          return (
            <li className="product-list__item">
              <span>
                {product.title} - ( {product.stock} )
              </span>
              <button onClick={() => setSoldOut(product.id)}>Sold out</button>
              <button
                onClick={() =>
                  addStock(product.id, Math.ceil(Math.random() * 50 + 50))
                }
              >
                Add Stock
              </button>
            </li>
          );
        })}
      </ul>
    </div>
  );
}

```

## Dealing with the custom hook's performance issue:

So far, so good. But what if we need to pass these methods to the down in the component? Yes, it will re-render an unnecessary. How do you handle unnecessary re-rendering? Here [React.useCallback](https://reactjs.org/docs/hooks-reference.html#usecallback) and [React.useMemo](https://reactjs.org/docs/hooks-reference.html#usememo) come in handy.


```javascript
 // omitted unwanted snippet

export function useProduct() {
  const [{ products }, dispatch] = React.useReducer(productListReducer, {
    products: mockProducts,
  });

  const sortBy = useCallback((by: "rating" | "title") => {
    const type =
      by === "rating"
        ? ProductListActionKind.SORT_BY_RATING
        : ProductListActionKind.SORT_BY_NAME;
    dispatch({ type, payload:{}});
  }, []);

  const isProductsInStock = useMemo(() => {
    return products.every((p) => p.stock > 0);
  }, [products]);
  
  const setSoldOut = useCallback((id: number) => {
    dispatch({ type: ProductListActionKind.PRODUCT_SOLD_OUT, payload: { id } });
  }, []);

  const addStock = useCallback((id: number, stock: number) => {
    dispatch({
      type: ProductListActionKind.ADD_STOCK_PRODUCT,
      payload: { id, stock },
    });
  }, []);

 return [
  {
    products,
    isProductsInStock,
  },
  {
    sortBy,
    setSoldOut,
    addStock,
  },
  ];

}

```

## Custom Hook Expert Level:

The pattern below is an excellent way to avoid using **useCallback** multiple times with **useMemo**; it is more concise and cleaner than using useCallback multiple times.

```javascript
const handlers = React.useMemo(
  () => ({
    sortBy(by: "rating" | "title") {...},
    setSoldOut(id: number) {...},
    addStock(id: number, quantity: number) {...},
  }),
  []
);
```

```javascript
 // omitted unwanted code

export function useProduct() {
  const [{ products }, dispatch] = React.useReducer(productListReducer, {
    products: mockProducts,
  });

  const isProductsInStock = useMemo(() => {
    return products.every((p) => p.stock > 0);
  }, [products]);

  const handlers = useMemo(
    () => ({
      sortBy(by: "rating" | "title") {
        const type =
          by === "rating"
            ? ProductListActionKind.SORT_BY_RATING
            : ProductListActionKind.SORT_BY_NAME;
        dispatch({ type, payload: {} });
      },
      setSoldOut(id: number) {
        dispatch({
          type: ProductListActionKind.PRODUCT_SOLD_OUT,
          payload: { id },
        });
      },
      addStock(id: number, quantity: number) {
        dispatch({
          type: ProductListActionKind.ADD_STOCK_PRODUCT,
          payload: { id, stock: quantity },
        });
      },
    }),
    []
  );

  return [
    { products, isProductsInStock },
    handlers
  ] as const;
}

```

## Bonus:

How do we take our `useProduct` custom hook to the next level?

What if we need to create a `CategoryList` with `useCategory` custom hook with operations such as _addCategory, removeCategory, and updateCategory, sortByCategory_ and so on ? that is very similar to a `ProductList` and `useProducts`

You may encounter code duplication because the same data structures are used in underlying. so it's time to create a new custom hook!, let's call it **useArray** with these features _add, remove, update and sortBy_ and so on.

```javascript 
const [data, {add, update, remove, sortBy}] = useArray(initialState)
```
We can now **remove** the `reducer function` and its implementation from the `useProduct` and `useCategory` custom hook, making both domain custom hooks and components more concise.

## Wrap up: 

Custom hooks are a natural way to encapsulate and abstract business logic, and they allow us to create very powerful abstractions for React applications.

Use your imagination to take it to the next level.

Good luck!