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

Веб-приложения управляют большими объёмами данных: каталоги товаров, поисковые результаты, ленты действий. Выводить всё в одну страницу — значит получить медленную загрузку и плохой пользовательский опыт. Пагинация решает эту проблему, разбивая данные на удобные страницы.
Пагинация бывает двух больших подходов:
- клиентская пагинация — данные подгружены в браузер и разбиваются на страницы на клиенте;
- серверная пагинация — сервер возвращает только один фрагмент данных (страницу) по запросу.
В этой статье мы реализуем клиентскую постраничную навигацию в React с использованием хуков и обсудим альтернативы, тесты и проверки.
Когда подойдёт клиентская пагинация
Client-side подходит, когда общий объём данных относительно невелик и может быть загружен целиком без проблем с памятью или трафиком. Примеры:
- демо-данные или небольшие списки (до нескольких тысяч записей, в зависимости от контекста);
- интерфейсы с частой навигацией между страницами, где повторные запросы на сервер нежелательны;
- когда не требуется строгая согласованность с серверным состоянием в реальном времени.
Важно: если данные большие, динамически растут или требуются строгие показатели консистентности, предпочитайте серверную или курсорную пагинацию.
Подходы к клиентской пагинации
Основные варианты разбивки данных на клиенте:
- Пагинация по страницам (page-based). Данные делятся на фиксированные страницы. Пользователь выбирает номер страницы либо кликает Prev/Next.
- Ленивый рендеринг и виртуализация. Отрисовываем в DOM только видимую часть списка, полезно для длинных списков.
- Бесконечная прокрутка (infinite scroll). Загружаем следующую порцию при скролле вниз — удобно для лент, но сложнее для навигации и доступа к конкретным позициям.
Каждый подход имеет свои плюсы и минусы. Ниже — практическая реализация page-based пагинации.
Минимальный компонент пагинации на React с хуками
Задача: получить набор задач todo из публичного API jsonplaceholder, разбить на страницы и отрисовать навигацию с Prev / Next и номерами страниц.
Создаём файл компонента, например src/components/Pagination.jsx.
Пример рабочего компонента с комментариями и улучшениями по доступности и производительности:
import React, { useEffect, useMemo, 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] = useState(5)
const [maxPageNumberLimit, setMaxPageNumberLimit] = useState(5)
const [minPageNumberLimit, setMinPageNumberLimit] = useState(0)
// загрузка данных один раз при монтировании
useEffect(() => {
let mounted = true
fetch('https://jsonplaceholder.typicode.com/todos')
.then(res => res.json())
.then(json => {
if (mounted) setData(json)
})
.catch(err => console.error('fetch error', err))
return () => { mounted = false }
}, [])
// вычисляем массив номеров страниц и элементы текущей страницы
const pages = useMemo(() => {
const total = Math.ceil(data.length / itemsPerPage)
return Array.from({ length: total }, (_, i) => i + 1)
}, [data.length, itemsPerPage])
const indexOfLastItem = currentPage * itemsPerPage
const indexOfFirstItem = indexOfLastItem - itemsPerPage
const pageItems = data.slice(indexOfFirstItem, indexOfLastItem)
// отрисовка списка
const displayData = items => (
{items.length === 0 ? (
- Нет данных для отображения
) : (
items.map((todo, idx) => - {todo.title}
)
)}
)
const handleClick = event => {
const page = Number(event.target.id)
setCurrentPage(page)
}
const renderPageNumbers = pages.map(number => {
if (number <= maxPageNumberLimit && number > minPageNumberLimit) {
return (
{ if (e.key === 'Enter') handleClick(e) }}
>
{number}
)
} else {
return null
}
})
const handleNextbtn = () => {
setCurrentPage(prev => prev + 1)
if (currentPage + 1 > maxPageNumberLimit) {
setMaxPageNumberLimit(prev => prev + pageNumberLimit)
setMinPageNumberLimit(prev => prev + pageNumberLimit)
}
}
const handlePrevbtn = () => {
setCurrentPage(prev => prev - 1)
if ((currentPage - 1) % pageNumberLimit === 0) {
setMaxPageNumberLimit(prev => prev - pageNumberLimit)
setMinPageNumberLimit(prev => prev - pageNumberLimit)
}
}
return (
<>
Компонент пагинации
{displayData(pageItems)}
-
{renderPageNumbers}
-
>
)
}
export default PaginationВажно: в коде использованы общие улучшения — useMemo для вычисления страниц, aria-атрибуты и обработчики клавиатуры для доступности. Также рекомендуется добавить обработку ошибок и состояние загрузки.
Пояснение ключевых шагов
Получение данных через fetch в useEffect и сохранение в state.
Вычисление общего числа страниц: ceil(totalItems / itemsPerPage).
Выбор части массива для текущей страницы: slice(indexOfFirstItem, indexOfLastItem).
Отрисовка списка pageItems вместо полного массива data.
Навигация: номера страниц, Prev и Next, управление видимым диапазоном номеров.
Улучшения и проверенные приёмы
- Кэширование данных: если данные статичны, кэшируйте результат fetch в локальном хранилище или в память, чтобы не перезапрашивать при переходах.
- Виртуализация (react-window / react-virtualized): если отображаемый список может содержать сотни или тысячи DOM-элементов, используйте виртуализацию вместо традиционной пагинации для гладкого скролла.
- Пагинация на сервере: для больших данных предпочтительна серверная пагинация или курсорная пагинация, чтобы уменьшить объём передаваемых данных.
- Показать всего N страниц с навигацией вперед/назад: полезно для длинных наборов страниц.
- Поддержка URL-параметров: синхронизируйте currentPage с query string, чтобы можно было делиться ссылками и делать back/forward в браузере.
Альтернативные подходы и когда они уместны
- Серверная пагинация (page-based): сервер возвращает нужную страницу. Уместно при больших объёмах и при необходимости актуальных данных.
- Курсорная пагинация (cursor-based): стабильно при изменяющихся данных, если важна консистентность при параллельных изменениях.
- Infinite scroll: хорош для лент, но плох для доступа к конкретной позиции и истории браузера.
- Комбинированный подход: серверно-страничный API + клиентская буферизация нескольких страниц для быстрой навигации.
Аксессибельность и i18n
- Кнопки должны быть фокусируемыми и управляемы с клавиатуры.
- Используйте aria-label и aria-live для уведомления о смене контента.
- Локализуйте тексты кнопок и числовой формат в зависимости от локали.
Важно: не полагайтесь только на цвета для обозначения активной страницы — добавьте видимый фокус и aria-current там, где это уместно.
Тесты и критерии приёмки
Критерии приёмки для компонента пагинации:
- При загрузке с 50 элементами и itemsPerPage 10 отображается 10 элементов и 5 страниц.
- Нажатие на номер страницы устанавливает currentPage и обновляет список.
- Prev и Next работают корректно и дизейблятся на краях.
- Изменение itemsPerPage сбрасывает currentPage на 1 и корректно пересчитывает страницы.
- Навигация фокусируема с клавиатуры и поддерживает Enter/Space.
- При ошибке fetch отображается сообщение об ошибке и возможность повторить запрос.
Минимальные тест-кейсы:
- Unit: вычисление pages при различных длинах data и itemsPerPage.
- Integration: поведение renderPageNumbers при изменении currentPage и limits.
- E2E: симуляция кликов и проверка DOM-элементов на странице.
Рольовые чеклисты перед релизом
Разработчик:
- Обработаны ошибки fetch.
- Добавлена поддержка клавиатуры и aria-атрибутов.
- Написаны модульные тесты для логики пагинации.
QA:
- Проверена навигация Prev/Next/номера страниц.
- Проверено поведение при изменении itemsPerPage.
- Проверено состояние при пустых данных и ошибках сети.
PM / Дизайнер:
- Подтверждены тексты и локализация.
- Подтверждён макет для разных состояний (много страниц, мало элементов).
Ментальные модели и хинты при проектировании
- «Разбивай по видимому»: показывай только ту часть данных, которую пользователь видит тут и сейчас.
- «Снижение трения навигации»: если пользователь часто переключается между страницами, предзагружай соседние страницы.
- «Консервативное государство»: при изменении количества элементов на странице всегда возвращай пользователя к валидному номеру страницы (обычно 1).
Типичные ошибки и когда решение не сработает
- Загрузка всей базы данных в браузер на мобильном устройстве приведёт к OOM и медленному UI.
- Infinite scroll улучшает вовлечение, но ухудшает способность пользователей попасть к конкретному элементу и делиться ссылками.
- Неправильная синхронизация URL приведёт к невозможности листать историю браузера или делиться текущим состоянием.
Производительность и масштабирование
- Используйте useMemo и избегайте лишних перерендеров списка.
- Для больших списков используйте виртуализацию вместо создания большого числа DOM-элементов.
- Подумайте о lazy-loading изображений в элементах списка.
Риски и смягчения
- Большой объём данных в клиенте: перейти на серверную или курсорную пагинацию.
- Нестабильный API: добавить retry и friendly error UI.
- Проблемы доступности: добавить автоматические проверки a11y и ручное тестирование с клавиатурой и скринридером.
Короткое объявление для команды (100–200 слов)
Добавлен компонент клиентской пагинации для списка задач. Компонент поддерживает: выбор количества элементов на странице, номера страниц, Prev/Next, keyboard navigation и aria-атрибуты. Логика использует хуки React, оптимизирована с помощью useMemo. Для больших объёмов данных рекомендуется перейти на серверную пагинацию или применить виртуализацию. Перед релизом проверьте сценарии ошибки сети и убедитесь, что стили для активного состояния соответствуют дизайну.
Сводка и рекомендации
- Для небольших наборов данных client-side пагинация простая и быстрая в реализации.
- Для больших данных используйте server-side или cursor-based подход.
- Не забывайте про доступность, URL-синхронизацию и тестирование.
Important: Пагинация — это не только техническая реализация. Это продуктовый выбор, влияющий на навигацию, SEO и аналитику.
Краткие выводы:
- Реализуйте пагинацию там, где это улучшит UX и производительность.
- Всегда оценивайте объём данных и ожидания по консистентности перед выбором клиентской или серверной стратегии.
Похожие материалы
Мемоизация в JavaScript и React
Как сменить поисковик по умолчанию в браузере
Как почистить PS4 — подробный русскоязычный гид
Установка и использование Nest Thermostat
Как отправить Word на Kindle быстро