Jennifer De Goede
Jennifer De Goede

Reputation: 459

Custom pagination - React table

I want to make a custom pagination using react-table. It needs to look quite a specific way:

Currently it looks like this, I guess this is the default for the current pagination I am using:

enter image description here

I need it to look like this:

enter image description here

My code is the following:

import React, { useEffect, useState } from 'react';
import { useTable, usePagination } from 'react-table';
import { TableProps } from '../../../types';

export const Table: React.FC<TableProps> = ({
  columns,
  data,
  isLoading,
  hasPagination = true,
  pageNumber,
  onPageChange,
  maxPage,
  onRowClick = () => undefined,
  maxCount,
}) => {
  const [canPreviousPage, setCanPreviousPage] = useState(false);
  const [canNextPage, setCanNextPage] = useState(false);
  const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    prepareRow,
    page,
    pageCount,
    nextPage,
    previousPage,
  } = useTable(
    {
      columns,
      data,
      manualPagination: true,
      pageCount: maxPage,
    },
    usePagination,
  );

  useEffect(() => {
    if (pageNumber <= 1) {
      setCanPreviousPage(false);
    } else {
      setCanPreviousPage(true);
    }
    if (maxPage > pageNumber) {
      setCanNextPage(true);
    } else {
      setCanNextPage(false);
    }
  }, [pageNumber, maxPage]);

  const onNextPage = () => {
    nextPage();
    onPageChange(pageNumber + 1);
  };

  const onPreviousPage = () => {
    previousPage();
    onPageChange(pageNumber - 1);
  };

  const Pagination = () => {
    if (!hasPagination) {
      return null;
    }

    return (
      <div className="pagination flex items-center text-black mt-3">
        <Button onClick={onPreviousPage} disabled={!canPreviousPage}>
          <Icon className={canPreviousPage ? 'color-black' : 'color-grey-200'}>chevron_left</Icon>
        </Button>
        <span>
          Page{' '}
          <strong>
            {pageNumber} of {pageCount}
          </strong>
        </span>
        <Button onClick={onNextPage} disabled={!canNextPage}>
          <Icon className={canNextPage ? 'color-black' : 'color-grey-200'}>chevron_right</Icon>
        </Button>
        <div>{maxCount} records found</div>
        <div className="mx-1">
          <ActivityLoader isLoading={isLoading} />
        </div>
      </div>
    );
  };

  return (
    <div className="pt-10 pl-20 pr-16 font-nunito font-medium text-base">
      <div className="align-middle inline-block w-full w-40 text-left">
        <div className="shadow border-b border-gray-200">
          <table {...getTableProps} className="w-full divide-y divide-gray-200">
            <thead className="bg-mainBlue p-16">
              {headerGroups.map((headerGroup) => (
                <tr {...headerGroup.getHeaderGroupProps()}>
                  {headerGroup.headers.map((column) => (
                    <th
                      scope="col"
                      className="px-6 py-4 text-left font-medium text-gray-500 p-16 bg-mainBlue text-base"
                      {...column.getHeaderProps()}
                    >
                      {column.render('Header')}
                    </th>
                  ))}
                </tr>
              ))}
            </thead>
            <tbody {...getTableBodyProps()} className="bg-white divide-y divide-gray-200">
              {page.map((row) => {
                prepareRow(row);
                return (
                  <tr {...row.getRowProps()} onClick={() => onRowClick(_.get(row, 'original.id'))}>
                    {row.cells.map((cell) => {
                      return (
                        <td
                          className="px-6 py-4 whitespace-nowrap text-black"
                          {...cell.getCellProps()}
                        >
                          {cell.render('Cell')}
                        </td>
                      );
                    })}
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      </div>
      <Pagination />
    </div>
  );
};

Can anyone help me to customize my pagination?? I have been reading some docs but I am struggling to implement a solution

enter image description here

Upvotes: 0

Views: 6196

Answers (2)

junwen-k
junwen-k

Reputation: 3644

When doing pagination using TanStack Table, you should generally follow the official guide by utilizing pagination state and onPaginationChange. Using pagination state in your React Table as your source of truth is advised — Many logic has been handled by the library, you shouldn't need a separate state canPreviousPage and should directly utilize table.getCanPreviousPage() instead.

Regarding to pagination range, you can use this hook written based on Mantine's usePagination. The hook read existing pagination state directly from table instance and computes the pagination range.

use-react-table-pagination-range.ts

import type { Table } from '@tanstack/react-table';

const range = (start: number, end: number) => {
  const length = end - start + 1;
  return Array.from({ length }, (_, index) => index + start);
};

export const ELLIPSIS = 'ellipsis';

export const useReactTablePaginationRange = <TData>(
  table: Table<TData>,
  /**
   * Siblings amount on left/right side of selected page, defaults to 1.
   */
  siblings = 1,
  /**
   * Amount of elements visible on left/right edges, defaults to 1.
   */
  boundaries = 1
) => {
  const total = table.getPageCount();
  const activePage = table.getState().pagination.pageIndex + 1;

  const totalPageNumbers = siblings * 2 + 3 + boundaries * 2;
  if (totalPageNumbers >= total) {
    return range(1, total);
  }

  const leftSiblingIndex = Math.max(activePage - siblings, boundaries);
  const rightSiblingIndex = Math.min(activePage + siblings, total - boundaries);

  const shouldShowLeftDots = leftSiblingIndex > boundaries + 2;
  const shouldShowRightDots = rightSiblingIndex < total - (boundaries + 1);

  if (!shouldShowLeftDots && shouldShowRightDots) {
    const leftItemCount = siblings * 2 + boundaries + 2;
    return [
      ...range(1, leftItemCount),
      ELLIPSIS,
      ...range(total - (boundaries - 1), total),
    ] as const;
  }

  if (shouldShowLeftDots && !shouldShowRightDots) {
    const rightItemCount = boundaries + 1 + 2 * siblings;
    return [
      ...range(1, boundaries),
      ELLIPSIS,
      ...range(total - rightItemCount, total),
    ] as const;
  }

  return [
    ...range(1, boundaries),
    ELLIPSIS,
    ...range(leftSiblingIndex, rightSiblingIndex),
    ELLIPSIS,
    ...range(total - boundaries + 1, total),
  ] as const;
};

Usage

import { useReactTable } from "@tanstack/react-table"

import { ELLIPSIS, useReactTablePaginationRange } from "@/components/hooks/use-react-table-pagination-range";

const Example = () => {
  const table = useReactTable({ ... });

  const paginationRange = useReactTablePaginationRange(table);

  return (
    <>
        ...
        {paginationRange.map((page, index) => (
          <div key={index}>
            {page === ELLIPSIS ? (
              <span>…</span>
            ) : (
              <button
                onClick={() => table.setPageIndex(page - 1)}
              >
                {page}
              </button>
            )}
          </div>
        ))}
        ...
    </>
  )
}

Note that accessibility and other details are omitted for brevity.

You can take a look at this example modified directly from the official example.

Edit tanstack-table-pagination-range

Upvotes: 0

yohanes
yohanes

Reputation: 2675

First, create a new file called usePaginationPages.js

import { useCallback, useEffect, useMemo, useState } from "react";

export const usePaginationPages = ({ gotoPage, length, pageSize }) => {
  const [currentPage, setCurrentPage] = useState(1);

  const totalPages = useMemo(() => {
    return Math.ceil(length / pageSize);
  }, [length, pageSize]);

  const canGo = useMemo(() => {
    return {
      next: currentPage < totalPages,
      previous: currentPage - 1 > 0
    };
  }, [currentPage, totalPages]);

  // currentPage siblings
  const pages = useMemo(() => {
    const start = Math.floor((currentPage - 1) / 5) * 5;
    const end = start + 5 > totalPages ? totalPages : start + 5;
    return Array.from({ length: end - start }, (_, i) => start + i + 1);
  }, [currentPage, totalPages]);

  // programatically call gotoPage when currentPage changes
  useEffect(() => {
    gotoPage(currentPage - 1);
  }, [currentPage, gotoPage]);

  // show first page when per page select options changes 
  useEffect(() => {
    if (pageSize) {
      goTo(1);
    }
  }, [pageSize]);

  const goTo = (pg) => {
    setCurrentPage(pg);
  };

  const goNext = useCallback(() => {
    if (canGo.next) {
      setCurrentPage((prev) => prev + 1);
    }
  }, [canGo]);

  const goPrev = useCallback(() => {
    if (canGo.previous) {
      setCurrentPage((prev) => prev - 1);
    }
  }, [canGo]);

  return {
    canGo,
    currentPage,
    pages,
    goTo,
    goNext,
    goPrev
  };
};

Next, create Pagination.js by implementing our usePaginationPages hook

import { useState, useEffect, memo } from "react";
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/solid";
import { usePaginationPages } from "./usePaginationPages";

function Pagination({ gotoPage, length, pageSize, setPageSize }) {
  const [perPage, setPerPage] = useState(pageSize);

  const {
    canGo,
    currentPage,
    pages,
    goTo,
    goNext,
    goPrev
  } = usePaginationPages({
    gotoPage,
    length,
    pageSize
  });
 
  // update pageSize when perPage changes
  useEffect(() => {
    // don't forget set to Number
    setPageSize(Number(perPage));
  }, [perPage, setPageSize]);

  return (
    <div className="m-4 flex items-center justify-center">
      <button
        onClick={goPrev}
        disabled={!canGo.previous}
        className="m-1 px-2 py-1 border rounded-md"
      >
        <ChevronLeftIcon className="h-6 w-4 text-blue-500" />
      </button>
      {pages.map((page, i) => (
        <button
          onClick={() => goTo(page)}
          key={i}
          style={{
            background: currentPage === page ? "blue" : "none",
            color: currentPage === page ? "white" : "black"
          }}
          className="m-1 px-3 py-1 border rounded-md"
        >
          {page}
        </button>
      ))}
      <button
        onClick={goNext}
        disabled={!canGo.next}
        className="m-1 px-2 py-1 border rounded-md"
      >
        <ChevronRightIcon className="h-6 w-4 text-blue-500" />
      </button>
      <select
        className="px-2 py-[6px] border rounded-md w-30 bg-white"
        value={pageSize}
        onChange={(e) => setPerPage(e.target.value)}
      >
        {[10, 50, 100].map((pageSize) => (
          <option className="py-2" value={pageSize} key={pageSize}>
            {pageSize} / page
          </option>
        ))}
      </select>
    </div>
  );
}

export default memo(Pagination);

You can see the full code here:

Javascript version:

Edit quiet-browser-z5duq2

Typescript version:

Edit awesome-murdock-2heycu

Upvotes: 2

Related Questions