Пагинация в React: клиентская реализация с хуками

Зачем нужна пагинация
Веб-приложения часто работают с большими объёмами данных: каталоги товаров, ленты новостей, отчёты. Рендерить все элементы сразу — плохая идея. Это ведёт к долгой загрузке, повышенному потреблению памяти и плохому UX. Пагинация разбивает набор данных на страницы и показывает пользователю управляемые порции.
Важно: “пагинация” — это общий приём. Он не отменяет необходимость оптимизации запросов, кеширования и ленивой загрузки.
Варианты реализации клиентской пагинации
Клиентская пагинация означает, что все данные загружены в браузер, а логика показа страниц выполняется локально. Основные подходы:
- Постраничная пагинация (page-based). Деление на фиксированные страницы по N элементов. Подходит для каталогов, поисковых результатов.
- Бесконечная прокрутка (infinite scroll). Данные подгружаются по мере прокрутки. Подходит для фидов и контента, где пользователь скроллит последовательно.
Когда выбрать клиентскую пагинацию:
- Объём данных ограничен и может быть загружен в память (обычно до нескольких тысяч записей в зависимости от клиента).
- Нужен быстрый отклик при переключении страниц без повторных запросов.
Когда она не подходит:
- Данные огромные (десятки тысяч/миллионы записей).
- Нужны обновления на сервере в реальном времени, и важно показывать только актуальные страницы.

Быстрый пример: постраничная пагинация в React с хуками
Ниже показан практический пример. Предположим, проект создан через Vite или create-react-app. Компонент размещаем в src/components/pagination.
1) Стартовый компонент и состояния
Создайте React-функциональный компонент и определите состояния:
import React, { useEffect, useState } from "react";
import "./style.component.css";
function Pagination() {
const [data, setData] = useState([]);
const [currentPage, setcurrentPage] = useState(1);
const [itemsPerPage, setitemsPerPage] = useState(5);
const [pageNumberLimit, setpageNumberLimit] = useState(5);
const [maxPageNumberLimit, setmaxPageNumberLimit] = useState(5);
const [minPageNumberLimit, setminPageNumberLimit] = useState(0);
return (
<>
Pagination Component
>
);
}
export default Pagination;Объяснение переменных в одной строке:
- data — массив всех записей, загруженных в компонент.
- currentPage — текущая страница (номер).
- itemsPerPage — сколько элементов показываем на странице.
- pageNumberLimit — сколько номеров страниц показываем в навигации одновременно.
- maxPageNumberLimit, minPageNumberLimit — окно видимых номеров страниц.
2) Получение данных и рендер списка
Пример получения данных из JSONPlaceholder и простая функция отображения:
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/todos")
.then((response) => response.json())
.then((json) => setData(json));
}, []);
const displayData = (data) => {
return (
{data.length > 0 &&
data.map((todo, index) => {
return - {todo.title}
;
})}
);
};Затем в return показываем список:
return (
<>
Pagination Component
{displayData(data)}
>
);Этот код выведет все загруженные элементы. Если элементов много — UX пострадает. Далее реализуем ограничение по страницам.
3) Логика расчёта страниц и выбор элементов для текущей страницы
Определяем количество страниц и элементы для отображения:
const pages = [];
for (let i = 1; i <= Math.ceil(data.length / itemsPerPage); i++) {
pages.push(i);
}Затем вычисляем индексы и извлекаем подмассив для текущей страницы:
const indexOfLastItem = currentPage * itemsPerPage;
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
const pageItems = data.slice(indexOfFirstItem, indexOfLastItem);И используем displayData для отображения только pageItems:
return (
<>
Pagination Component
{displayData(pageItems)}
>
);Теперь переключение currentPage влияет на показимый поднабор.
4) Кнопки навигации и номера страниц
Добавим обработчик клика по номеру страницы и генерацию списка номеров:
const handleClick = (event) => {
setcurrentPage(Number(event.target.id));
};
const renderPageNumbers = pages.map((number) => {
if (number < maxPageNumberLimit +1 && number > minPageNumberLimit) {
return (
{number}
);
} else {
return null;
}
});Обработчики Prev/Next:
const handleNextbtn = () => {
setcurrentPage(currentPage + 1);
if (currentPage + 1 > maxPageNumberLimit) {
setmaxPageNumberLimit(maxPageNumberLimit + pageNumberLimit);
setminPageNumberLimit(minPageNumberLimit + pageNumberLimit);
}
};
const handlePrevbtn = () => {
setcurrentPage(currentPage - 1);
if ((currentPage - 1) % pageNumberLimit == 0) {
setmaxPageNumberLimit(maxPageNumberLimit - pageNumberLimit);
setminPageNumberLimit(minPageNumberLimit - pageNumberLimit);
}
};И окончательная разметка навигации:
return (
<>
Pagination Component
{displayData(pageItems)}
-
{renderPageNumbers}
-
>
);Важно: подписи на кнопках в коде сохранены как в исходнике. При локализации UI можно заменить “Previous”/“Next” на русский текст.
Доступность и UX
- Добавьте aria-метки и правильные роли (role=”navigation”) для кнопок и списка страниц.
- Обеспечьте фокусируемость и управление с клавиатуры (Enter/Space для кнопок страниц).
- Подумайте о состоянии загрузки (skeleton/loader) при переключении страниц.
- Для мобильных — уменьшите количество одновременных номеров страниц, используйте сжатые контролы.
Когда выбрать готовую библиотеку
Готовые библиотеки (например, react-paginate) ускоряют разработку. Они полезны если:
- Вам нужно быстрое, проверенное решение.
- Требуется консистентный UI и поддержка edge case’ов.
- Вы не хотите тратить время на тестирование и полировку поведения переключения страниц.
Минусы библиотек:
- Меньше гибкости для нестандартного UI.
- Размер библиотеки может быть избыточен для простого кейса.
Когда необходима серверная пагинация
Серверная пагинация — логика запросов и разбиение результатов выполняется на сервере. Её стоит выбирать, если:
- Наборы данных большие (тысячи+ записей) и полная загрузка в браузер невозможна.
- Нужна сортировка/фильтрация на сервере.
- Экономия трафика и ускорение первого отображения важнее гибкости локального переключения.
Альтернативы и гибриды
- Комбинированная схема: серверная пагинация + клиентский кеш для недавних страниц.
- Виртуализированный список (react-window, react-virtualized) вместо пагинации для длинных списков, когда важна плавная прокрутка.
- Бесконечная прокрутка с опцией “Загрузить ещё” для контроля пользователя.
Ментальные модели и эвристики
- “Меньше на экране — легче воспринять”: держите 10–50 элементов на страницу для списков с короткими карточками.
- “Пользовательская цель важнее”: если пользователь ищет конкретный элемент — серверный поиск + фильтры важнее бесконечного скролла.
- “Локальная скорость vs свежесть данных”: клиентская пагинация быстрее при навигации, но может показывать устаревшие данные.
Критерии приёмки
- UI показывает не более itemsPerPage элементов на странице.
- Переключение страниц изменяет содержимое без перезагрузки страницы.
- Кнопки Previous и Next корректно блокируются на границах.
- Номера страниц корректно отображаются с учётом min/max окна.
- Адаптивность: навигация остаётся удобной на мобильных.
- Доступность: элементы управления доступны с клавиатуры и имеют aria-атрибуты.
Руководство по внедрению (пошаговый SOP)
- Оцените объём данных. Если > несколько тысяч — планируйте серверную пагинацию или виртуализацию.
- Выберите число itemsPerPage по UX (10–20 для таблиц, 20–50 для карточек).
- Реализуйте базовый компонент и напишите unit-тесты для функционала переключения и расчёта индексов.
- Добавьте индикаторы загрузки и обработку ошибок сетевых запросов.
- Проведите ручное тестирование на мобильных и в старых браузерах.
- Прогоните accessibility audit (axe, Lighthouse).
- Подготовьте документацию для команды и тест-кейсы для QA.
Чеклист ролей
- Developer:
- Реализовал расчёт страниц и навигацию.
- Добавил aria-атрибуты и фокус-менеджмент.
- Покрыл ключевые функции unit-тестами.
- QA:
- Проверил переключение страниц, блокировку Prev/Next, крайние случаи при 0 и 1 элементе.
- Тестировал на мобильных и с клавиатурой.
- Product / PM:
- Подтвердил число элементов на странице.
- Утвердил поведение при отсутствии данных и при ошибках сети.
Тесты и критерии приёмки (минимум)
- Unit: расчёт indexOfFirstItem/indexOfLastItem при разных currentPage и itemsPerPage.
- Integration: переключение на страницу X отображает ожидаемые элементы.
- E2E: клики Prev/Next и по номеру страницы ведут к корректному состоянию и URL (если используется роутинг).
Примеры отказов и подводные камни
- Попытка грузить весь каталог из базы в браузер при миллионах записей — приведёт к OOM и медленной работе.
- Несоответствие itemsPerPage между сервером и клиентом — возможны дубли и пропуски.
- Отсутствие обработки ошибок fetch — баги при недоступном API.
Небольшой чек: добавление URL-синхронизации
Для удобства и восстановления состояния добавьте синхронизацию currentPage с query-параметром URL (например, ?page=3). Это даёт:
- Возможность отправить ссылку на конкретную страницу.
- Сохранение состояния при перезагрузке.
Пример (React Router): обновляйте query при изменении currentPage и читайте его при mount.
Decision flow (выбор подхода)
flowchart TD
A[Нужна пагинация?] --> B{Объём данных}
B --> |Малый / Средний| C[Клиентская пагинация с хуками]
B --> |Большой| D[Серверная пагинация или виртуализация]
C --> E{Требуется бесконечный UX?}
E --> |Да| F[Infinite scroll + опционально 'Загрузить ещё']
E --> |Нет| G[Стандартная постраничная навигация]
D --> H[API с параметрами page & limit]Пример улучшенного компонента (шаблон)
Ниже — компактный шаблон компонента (сохраните логику расчётов и aria атрибуты при адаптации в проект):
import React, { useEffect, useState } from 'react';
export default function PaginationExample() {
const [data, setData] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage] = useState(10);
useEffect(() => {
let mounted = true;
fetch('https://jsonplaceholder.typicode.com/todos')
.then(r => r.json())
.then(json => { if (mounted) setData(json); })
.catch(() => { if (mounted) setData([]); });
return () => { mounted = false; };
}, []);
const totalPages = Math.max(1, Math.ceil(data.length / itemsPerPage));
const changePage = (num) => {
setCurrentPage(Math.min(Math.max(1, num), totalPages));
};
const start = (currentPage - 1) * itemsPerPage;
const pageItems = data.slice(start, start + itemsPerPage);
return (
);
}Этот пример показывает более безопасную загрузку, защиту от выхода за границы страниц и базовую доступность.
Итог и рекомендации
- Для небольших и среднего размера наборов данных клиентская пагинация на хуках — простое и быстрое решение.
- Для больших объёмов используйте серверную пагинацию или виртуализацию.
- Всегда добавляйте индикаторы загрузки, обработку ошибок и accessibility.
- Подумайте о синхронизации с URL для улучшения UX и возможности делиться ссылками.
Краткое резюме: пагинация — это не только кнопки. Это выбор архитектуры данных, UX и компромисс между скоростью и актуальностью.
Важно: тестируйте на реальных объемах и устройствах перед финальным релизом.
Похожие материалы
CHKDSK в Windows 10 — как проверить и исправить диск
Искать файлы Google Drive из адресной строки Chrome
Credential Manager в Windows — управление паролями
MailTrack в Opera: узнавайте когда прочли письма
Простой калькулятор на Python с Tkinter