DEV Community

Amir
Amir

Posted on • Edited on

Downsides of Passing object as `useState` hook value.

In this article, I'll talk about using objects with the useState hook. there are downsides to this approach. To explore this, let's consider a pagination component as an example. Although I'm using pagination here, similar considerations apply to other cases like when working with the useState hook. Imagine we have a <Pagination/> component.

Let's use it inside of the screen that you want to have a pagination.

  function Screen() {
     const [pagination, setPagination] = useState({
         ... data // bring some defautl data from some where.  
     }); 

     // bringing other pagination related functions in the body of this component

     return ( 
        <div> 
           <Pagiantion {...pagination}/>
        </div>
     )
  }

Enter fullscreen mode Exit fullscreen mode

Is this logic related to pagination functionality reusable? The answer is "No." This is because the reusable logic is placed on the consumer side. To reuse it, you would need to copy the code from the component and paste it elsewhere. This might sound straightforward, but it's not ideal. By encapsulating all the pagination data within an object, maybe people might miss the opportunity to create a reusable custom hook named usePagination.

This is one of the main reasons why I believe using objects as values for the useState is problematic. When we use an object as the value for the useState hook, we miss out on a significant chance for reusability. So, what's a better approach? The solution lies in writing a custom hook.

Let's take a step further and imagine that we've decided to implement a custom hook. However, even in this scenario, we continue to use an object for the useState hook. The code snippet below illustrates 2 version of the implementation of the usePagination hook.

export const usePagination = (paginationProperties) => {
  const [pageLimit, setPageLimit] = useState(paginationProperties?.pageSize || 40);
  const [current, setCurrent] = useState(paginationProperties?.currentPage || 1);
  const [hasNext, setHasNext] = useState(paginationProperties?.hasNext || false);
  const [hasPrevious, setHasPrevious] = useState(paginationProperties?.hasPrevious || false);
  const [totalPages, setTotalPages] = useState(paginationProperties?.totalPages || 0);
  const [totalCount, setTotalCount] = useState(paginationProperties?.totalCount || 0);

  const nextPage = () => {
    if (hasNext) {
      setCurrent((current + 1) % (totalPages + 1) || 1);
    }
  };

  const previousPage = () => {
    if (hasPrevious) {
      setCurrent((current - 1 + totalPages) % (totalPages + 1) || totalPages);
    }
  };

  const skipBack = () => setCurrent(1);  

  const skipForward = () =>  setCurrent(totalPages);

  const resetPagination = () => {
    setPageLimit(10);
    setCurrent(1);
  };

  const goToPage = (pageNumber: number) => {
    setCurrent(pageNumber);
  };

  // return everything that Pagination component needs to work with 
  return ...; 
};

Enter fullscreen mode Exit fullscreen mode
export const usePagination = (paginationProperties) => {
  const [pagination, setPagination] = useState({
    pageLimit: paginationProperties?.pageSize || 40,
    current: paginationProperties?.currentPage || 1,
    hasNext: paginationProperties?.hasNext || false,
    hasPrevious: paginationProperties?.hasPrevious || false,
    totalPages: paginationProperties?.totalPages || 0,
    totalCount: paginationProperties?.totalCount || 0,
  });

  const nextPage = () => {
    if (pagination.hasNext) {
      setPagination(prevPagination => ({
        ...prevPagination,
        current: (prevPagination.current + 1) % (prevPagination.totalPages + 1) || 1,
      }));
    }
  };

  const previousPage = () => {
    if (pagination.hasPrevious) {
      setPagination(prevPagination => ({
        ...prevPagination,
        current: (prevPagination.current - 1 + prevPagination.totalPages) % (prevPagination.totalPages + 1) || prevPagination.totalPages,
      }));
    }
  };

  const skipBack = () => setPagination(prevPagination => ({
    ...prevPagination,
    current: 1,
  }));

  const skipForward = () => setPagination(prevPagination => ({
    ...prevPagination,
    current: prevPagination.totalPages,
  }));

  const resetPagination = () => {
    setPagination(prevPagination => ({
      ...prevPagination,
      pageLimit: 10,
      current: 1,
    }));
  };

  const goToPage = (pageNumber) => {
    setPagination(prevPagination => ({
      ...prevPagination,
      current: pageNumber,
    }));
  };

  // return everything that Pagination component needs to work with 
  return ...;
};

Enter fullscreen mode Exit fullscreen mode

Let's delve into the drawbacks of using an object as the useState value:

complexity:
Let's compare the complexity of both hook versions and see which one is clearer. Clearly, the first version is more straightforward. Some argue that using an object as the hook value makes code tidier by consolidating related data. However, I find this less compelling. The usePagination hook, as its name suggests, should handle all relevant aspects. As the state object grows, managing and debugging can become more intricate. Another option is to use the useReducer hook, designed for situations where states need to be collocated.

Immutability:
When you're using the setState function or its equivalent with an object as the value, it's crucial to ensure immutability while updating a single property within that object. Sometimes, individuals resort to third-party libraries like 'Immer' to simplify managing immutability during updates. do you need another package for just updating your state ?

Shallow Comparison:
React's default behavior for comparing state updates involves shallow comparisons of objects, which might lead to unexpected behavior if not managed carefully.

Performance:
While using objects can be performant for many cases, extremely large or deeply nested state objects might impact performance.

In conclusion, using an object as the useState value in React might make your code look organized and readable, but it can be misleading. This method might hinder your ability to create custom hooks and understand your components' needs fully. You could miss chances to make your code more reusable.

Moreover, this approach requires you to carefully think about aspects like complexity, immutability, and potential performance issues. It's crucial to understand the downsides of using an object with useState.

Top comments (0)