eMoosavi
Weekly frontend dev braindumps
Implementing Custom Pagination Logic with React and Tanstack Table
API Integration

Implementing Custom Pagination Logic with React and Tanstack Table

Enhance your data tables with customized pagination strategies

Jul 22, 2024 - 20:565 min read

One of the essential elements for any dynamic web application is presenting data in an accessible and user-friendly manner. Often, this involves utilizing tables to display large datasets, requiring effective pagination to enhance user experience. In this blog post, we will explore how to implement custom pagination logic in a data table using React and Tanstack Table, a powerful table component library.

Why Custom Pagination?

Default pagination mechanisms work well for most applications, but sometimes you need tailored functionalities not provided by out-of-the-box solutions. Custom pagination allows you to cater specifically to your application’s needs, manage server-side paginations, and optimize client-side performance.

Setting Up Tanstack Table

First, let’s set up Tanstack Table in our React application. If you haven’t already, you’ll need to install the packages:

npm install @tanstack/react-table

Once installed, we can initiate Tanstack Table and configure basic columns and data states:

import React, { useMemo } from 'react';
import { useTable, usePagination } from '@tanstack/react-table';

const DataTable = ({ columns, data }) => {
  const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    prepareRow,
    page, // Use the page, instead of rows, to render the table
    canPreviousPage,
    canNextPage,
    pageOptions,
    pageCount,
    gotoPage,
    nextPage,
    previousPage,
    setPageSize,
    state: { pageIndex, pageSize },
  } = useTable(
    {
      columns,
      data,
    },
    usePagination
  );

  return (
    <>
      <table {...getTableProps()}>
        <thead>
          {headerGroups.map(headerGroup => (
            <tr {...headerGroup.getHeaderGroupProps()}>
              {headerGroup.headers.map(column => (
                <th {...column.getHeaderProps()}>{column.render('Header')}</th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody {...getTableBodyProps()}>
          {page.map(row => {
            prepareRow(row);
            return (
              <tr {...row.getRowProps()}>
                {row.cells.map(cell => {
                  return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>;
                })}
              </tr>
            );
          })}
        </tbody>
      </table>
      <div className="pagination">
        <button onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
          {'<<'}
        </button>{' '}
        <button onClick={() => previousPage()} disabled={!canPreviousPage}>
          {'<'}
        </button>{' '}
        <button onClick={() => nextPage()} disabled={!canNextPage}>
          {'>'}
        </button>{' '}
        <button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}>
          {'>>'}
        </button>{' '}
        <span>
          Page{' '}
          <strong>
            {pageIndex + 1} of {pageOptions.length}
          </strong>{' '}
        </span>
        <span>
          | Jump to:{' '}
          <input
            type="number"
            defaultValue={pageIndex + 1}
            onChange={e => {
              const page = e.target.value ? Number(e.target.value) - 1 : 0;
              gotoPage(page);
            }}
            style={{ width: '100px' }}
          />
        </span>{' '}
        <select
          value={pageSize}
          onChange={e => {
            setPageSize(Number(e.target.value));
          }}
        >
          {[10, 20, 30, 40, 50].map(pageSize => (
            <option key={pageSize} value={pageSize}>
              Show {pageSize}
            </option>
          ))}
        </select>
      </div>
    </>
  );
};

export default DataTable;

Custom Pagination Logic

Now that we have a basic table with pagination set up, let's move on to implementing custom pagination logic. Let's say, for example, you want to implement server-side pagination. This would involve fetching data from an API endpoint based on the current page and page size.

First, we need to modify our table component to handle server-side data fetching:

import React, { useEffect, useState, useMemo } from 'react';
import { useTable, usePagination } from '@tanstack/react-table';

const DataTable = ({ columns, fetchData, pageCount: controlledPageCount }) => {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(false);
  const [pageCount, setPageCount] = useState(controlledPageCount);

  const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    prepareRow,
    page,
    canPreviousPage,
    canNextPage,
    pageOptions,
    gotoPage,
    nextPage,
    previousPage,
    setPageSize,
    state: { pageIndex, pageSize },
  } = useTable(
    {
      columns,
      data,
      manualPagination: true,
      pageCount,
    },
    usePagination
  );

  useEffect(() => {
    const fetchDataAsync = async () => {
      setLoading(true);
      const fetchedData = await fetchData({ pageIndex, pageSize });
      setData(fetchedData.data);
      setPageCount(fetchedData.pageCount);
      setLoading(false);
    };

    fetchDataAsync();
  }, [fetchData, pageIndex, pageSize]);

  return (
    <>
      <table {...getTableProps()}>
        <thead>
          {headerGroups.map(headerGroup => (
            <tr {...headerGroup.getHeaderGroupProps()}>
              {headerGroup.headers.map(column => (
                <th {...column.getHeaderProps()}>{column.render('Header')}</th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody {...getTableBodyProps()}>
          {loading ? (
            <tr>
              <td colSpan="10000">Loading...</td>
            </tr>
          ) : (
            page.map(row => {
              prepareRow(row);
              return (
                <tr {...row.getRowProps()}>
                  {row.cells.map(cell => {
                    return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>;
                  })}
                </tr>
              );
            })
          )}
        </tbody>
      </table>
      <div className="pagination">
        <button onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
          {'<<'}
        </button>{' '}
        <button onClick={() => previousPage()} disabled={!canPreviousPage}>
          {'<'}
        </button>{' '}
        <button onClick={() => nextPage()} disabled={!canNextPage}>
          {'>'}
        </button>{' '}
        <button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}>
          {'>>'}
        </button>{' '}
        <span>
          Page{' '}
          <strong>
            {pageIndex + 1} of {pageOptions.length}
          </strong>{' '}
        </span>
        <span>
          | Jump to:{' '}
          <input
            type="number"
            defaultValue={pageIndex + 1}
            onChange={e => {
              const page = e.target.value ? Number(e.target.value) - 1 : 0;
              gotoPage(page);
            }}
            style={{ width: '100px' }}
          />
        </span>{' '}
        <select
          value={pageSize}
          onChange={e => {
            setPageSize(Number(e.target.value));
          }}
        >
          {[10, 20, 30, 40, 50].map(pageSize => (
            <option key={pageSize} value={pageSize}>
              Show {pageSize}
            </option>
          ))}
        </select>
      </div>
    </>
  );
};

export default DataTable;

Fetching Data from an API

Next, we need to set up a function to fetch data from an API. This function will be passed as the fetchData prop to the DataTable component:

const fetchData = async ({ pageIndex, pageSize }) => {
  const response = await fetch(
    `/api/data?page=${pageIndex}&size=${pageSize}`
  );
  const data = await response.json();

  return {
    data: data.items,
    pageCount: data.totalPages,
  };
};

You can adjust the API URL to match your backend setup. Ensure that your API paginates data and returns both the fetched items and the total number of pages.

Putting It All Together

Finally, let’s render our custom paginated table in the main component:

const columns = useMemo(
  () => [
    { Header: 'ID', accessor: 'id' },
    { Header: 'Name', accessor: 'name' },
    { Header: 'Age', accessor: 'age' },
  ],
  []
);

const App = () => {
  return (
    <div>
      <h1>Custom Paginated Table</h1>
      <DataTable columns={columns} fetchData={fetchData} pageCount={10} />
    </div>
  );
};

export default App;

Conclusion

Implementing custom pagination in React using the Tanstack Table library allows for a flexible and efficient way to handle large datasets. Whether you’re dealing with client-side or server-side data, the ability to tailor your pagination logic provides a significant advantage in creating responsive and user-friendly applications. This example demonstrates just one approach to customizing pagination, but the same principles can be applied to other unique needs and scenarios.

Article tags
reacttanstack-tablepaginationdata-handlingcustom-logic
Previous article

React Components

Reacting to Transitions: Advanced CSS Transition Handling in React

Next article

React Components

Efficiently Handling Form State in React with Immer and React-Hook-Form