В этом посте мы рассмотрим вариант создания пагинации для таблицы, которую мы создавали в прошлом посте. Также добавим переключатель количества записей на странице (pageSize).

Скачать готовую таблицу с прошлого примера вы можете тут. Рабочий пример с пагинацией доступен на CodeSandBox.

В папке components давайте создадим еще одну папку и назовем ее TablePagination. Там будет четыре файла:

Структура компонента TablePagination

В файле TablePagination.jsx добавим следующий код

import React, { useEffect, useState } from 'react';
import { Form, Pagination } from "react-bootstrap";
import './TablePagination.style.scss';
import usePagination, { DOTS } from "./usePagination";

const TablePagination = ({
   totalCount,
   changeItemsPerPage,
   pageSize,
   onPageChange,
   currentPage,
   siblingsCount = 1
}) => {
  const [totalPages, setTotalPages] = useState(Math.ceil(totalCount / pageSize));

  useEffect(() => {
    setTotalPages(Math.ceil(totalCount / pageSize));
  }, [pageSize]);

  const paginationRange = usePagination({ currentPage, totalCount, siblingsCount, pageSize })

  const handlePrevClick = () => {
    let changedPage = currentPage > 1 ? currentPage - 1 : currentPage
    onPageChange(changedPage);
  }

  const handleNextClick = () => {
    onPageChange(currentPage + 1);
  }

  const handleChangePageSize = (e) => {
    changeItemsPerPage(+e.target.value);
  }

  return (
    <div className='custom-table__pagination'>
      <Pagination>
        <Pagination.First onClick={() => onPageChange(1)} disabled={(totalCount <= pageSize)}/>
        <Pagination.Prev onClick={() => handlePrevClick()} disabled={currentPage === 1}/>
        {paginationRange.map((pageNumber, index) => {
          let current = index + 1;

          if (pageNumber === DOTS) {
            return <Pagination.Item key={index}>&#8230;</Pagination.Item>;
          }

          return <Pagination.Item
            key={index}
            active={currentPage === current}
            onClick={() => onPageChange(current)}
            disabled={(totalCount <= pageSize)}
          >
            {current}
          </Pagination.Item>
        })}
        <Pagination.Next onClick={() => handleNextClick()} disabled={currentPage === totalPages}/>
        <Pagination.Last onClick={() => onPageChange(totalPages)} disabled={(totalCount <= pageSize)}/>
      </Pagination>
      <Form.Select
        className='select-perpage'
        defaultValue={pageSize}
        onChange={(e) => handleChangePageSize(e)}
      >
        <option value="1">1</option>
        <option value="5">5</option>
        <option value="10">10</option>
        <option value="20">20</option>
      </Form.Select>
    </div>
  );
};

export default TablePagination;

Наш компонент в качестве пропсов получает:

  • totalCount — общее количество записей
  • changeItemsPerPage — хендлер для обработки изменения размера страницы (pageSize)
  • pageSize — количество отображаемых записей
  • onPageChange — хендлер для обработки изменения страницы
  • currentPage — текущая страница
  • siblingsCount = 1 — параметр, который устанавливает сколько отображать по соседству с текущей страницей других страниц (по умолчанию будет равен 1)

Далее мы определяем стейт totalPages — общее количество страниц. Его высчитываем в useEffect при изменении параметра pageSize. Чтобы правильно показать кол-во страниц округляем число в большую сторону с помощью Math.ceil.

Также для пагинации мы будем использовать кастомный хук — usePagination. Он будет нам возвращать массив со страницами. Вот код этого хука:

import { useMemo } from "react";

export const DOTS = '...';

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

const usePagination = ({ totalCount, pageSize, siblingsCount = 3, currentPage }) => {
  const paginationRange = useMemo(() => {
    const  totalPageCount = Math.ceil(totalCount / pageSize);

    const totalPageNumbers = siblingsCount + 5;

    if (totalPageNumbers >= totalPageCount) {
      return range(1, totalPageCount);
    }

    const leftSiblingIndex = Math.max(currentPage - siblingsCount, 1);
    const rightSiblingIndex = Math.min(currentPage + siblingsCount, totalPageCount);

    const shouldShowLeftDots = leftSiblingIndex > 2;
    const shouldShowRightDots = rightSiblingIndex < totalPageCount - 2;

    const firstPageIndex = 1;
    const lastPageIndex = totalPageCount;

    if (!shouldShowLeftDots && shouldShowRightDots) {
      let leftItemCount = 3 + 2 * siblingsCount;
      let leftRange = range(1, leftItemCount);

      return [...leftRange, DOTS, totalPageCount];
    }

    if (shouldShowLeftDots && !shouldShowRightDots) {
      let rightItemCount = 3 + 2 *siblingsCount;
      let rightRange = range(totalPageCount - rightItemCount + 1, totalPageCount);

      return [firstPageIndex, DOTS, ...rightRange];
    }

    if (shouldShowLeftDots && shouldShowRightDots) {
      let middleRange = range(leftSiblingIndex, rightSiblingIndex);
      return [firstPageIndex, DOTS, ...middleRange, DOTS, lastPageIndex];
    }

  }, [totalCount, pageSize, siblingsCount, currentPage]);

  return paginationRange;
}

export default usePagination;

Функция range будет возвращать нам массив в нужном диапазоне.

Также мы будем использовать хук useMemo, который будет сохранять наши результаты и перезапускать расчеты при изменении одной из зависимостей.

В зависимости от общего кол-ва страниц и от того на какой странице находимся мы, нам возможно понадобится добавлять вместо номера страницы три точки. Вот как раз эти сценарии и предусмотрены в коде (строки с 11-48).

Теперь осталось только в компоненте EditableTable вывести нашу пагинацию и добавить несколько строк кода:

const EditableTable = ({ columns, rows, actions }) => {
  /*... Старый код ... */

  const [pageSize, setPageSize] = useState(10);
  const [currentPage, setCurrentPage] = useState(1);

  useEffect(() => {
    if (currentPage >= rows.length / pageSize)
      setCurrentPage(1);
  }, [pageSize])

  useMemo(() => {
    const firstPageIndex = (currentPage - 1) * pageSize;
    const lastPageIndex = firstPageIndex + pageSize;

    const newData = rows.slice(firstPageIndex, lastPageIndex);

    setRowsState(newData);
  }, [currentPage, pageSize]);

return (
    <>
      {/*Оставляем вывод таблицы, я просто убрал, чтобы не загромождать код ... <Table striped bordered hover></Table>*/}
      <TablePagination
        totalCount={rows.length}
        pageSize={pageSize}
        changeItemsPerPage={page => setPageSize(page)}
        onPageChange={page => setCurrentPage(page)}
        currentPage={currentPage}
      />
    </>
  );
};

export default EditableTable;

Весь код вы может скачать по ссылке:

Ваши вопросы и комментарии: