Кастомная пагинация в React Native

Введение
Пагинация — это система деления большого объёма данных на страницы или порции, которые приложение загружает постепенно. Правильно реализованная кастомная пагинация уменьшает время отклика, экономит трафик и оперативную память устройства, а также делает интерфейс более отзывчивым.
Короткое определение: кастомная пагинация — логика приложения и API, которые совместно позволяют загружать и отображать ограниченное количество элементов за один раз.
Важно: термин “страница” может означать набор элементов фиксированного размера (page-based) или позицию в потоке данных (cursor-based). Ниже — практическое руководство с примерами для React Native и FlatList.
Что такое кастомная пагинация и зачем она нужна
Кастомная пагинация даёт контроль над:
- пользовательским интерфейсом навигации между страницами;
- стратегией выборки данных (page-based vs cursor-based);
- поведением при прокрутке (кнопка “Загрузить ещё” vs infinite scroll);
- обработкой ошибок, повторных попыток и индикаторов загрузки.
Основные преимущества
- Масштабируемость: приложение обрабатывает только нужные записи.
- Производительность: меньше сетевых запросов одного большого объёма.
- UX: плавная прокрутка и предсказуемая подгрузка.
- Гибкость: легко добавить фильтры, сортировку, кеширование.
Подходы к пагинации (краткое сравнение)
- Page-based (по страницам): API возвращает номера страниц и фиксированный pageSize. Простая реализация, подходит когда данные статичны.
- Cursor-based (по курсору): API возвращает указатель (cursor, nextToken). Лучший выбор для изменяющихся потоков данных и при больших объёмах.
- Offset-based: смещение + limit. Простейшая версия, но плохо масштабируется на больших таблицах.
- Infinite scroll: подгружает при прокрутке — удобен в мобильных лентах.
- Load More button: явный контроль со стороны пользователя, облегчает тестирование.
Мини‑методология: как выбрать стратегию
- Оцените объём и частоту изменений данных. Если данные часто изменяются — используйте cursor-based.
- Если нужен предсказуемый интерфейс и простота — page-based.
- Для лент и бесконечной прокрутки — infinite scroll + debounce по срабатыванию.
- Всегда добавляйте индикатор загрузки и обработку ошибок.
Пример ответа API (REST)
Ниже — пример простого REST-ответа, который возвращает массив книг и информацию о страницах:
{
"data": [
{
"id": 1,
"title": "The Catcher in the Rye",
"author": "J.D. Salinger"
},
{
"id": 2,
"title": "To Kill a Mockingbird",
"author": "Harper Lee"
}
// ...
],
"page": 1,
"totalPages": 5
}Определение: page — текущая страница, totalPages — общий номер страниц. Для cursor-based API вместо page вернётся cursor/nextCursor.
Функция выборки данных (fetchBooks)
Ниже — пример простого fetch-функциона для page-based API. Этот блок кода можно вставить в утилитный файл проекта (api.js или services/books.js).
const PAGE_SIZE = 10;
const fetchBooks = async (page) => {
try {
const response = await fetch(`https://myapi.com/books?page=${page}&pageSize=${PAGE_SIZE}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
return {
items: json.data || [],
page: json.page || page,
totalPages: json.totalPages || null
};
} catch (error) {
console.error('fetchBooks error:', error);
return { items: [], page, totalPages: null, error };
}
};
export { PAGE_SIZE, fetchBooks };Примечание: функция возвращает объект с items, page и totalPages — это облегчает логику управления состоянием на клиенте.
Простая реализация FlatList с подгрузкой (infinite scroll)
Ниже — упрощённый пример компонента с FlatList, который подгружает страницы при достижении конца списка. Код сохранён как рабочий пример.
import React, { useState, useEffect } from 'react';
import { FlatList, View, Text, ActivityIndicator } from 'react-native';
import { fetchBooks } from './api';
const App = () => {
const [books, setBooks] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [totalPages, setTotalPages] = useState(null);
useEffect(() => {
let mounted = true;
const load = async () => {
setIsLoading(true);
const res = await fetchBooks(1);
if (!mounted) return;
setBooks(res.items);
setTotalPages(res.totalPages);
setIsLoading(false);
};
load();
return () => { mounted = false; };
}, []);
const fetchMore = async () => {
if (isLoading) return;
if (totalPages && currentPage >= totalPages) return; // достигли конца
setIsLoading(true);
const nextPage = currentPage + 1;
const res = await fetchBooks(nextPage);
setCurrentPage(nextPage);
setIsLoading(false);
setBooks(prev => [...prev, ...res.items]);
};
const renderItem = ({ item }) => (
{item.title}
{item.author}
);
return (
item.id.toString()}
onEndReached={fetchMore}
onEndReachedThreshold={0.1}
ListFooterComponent={isLoading ? : null}
/>
);
};
export default App; Практические замечания:
- onEndReachedThreshold — значение 0..1, где 0.1 означает “когда осталось 10%”.
- ListFooterComponent — полезен для отображения индикатора загрузки.
- Добавьте дебаунс/троттлинг, если onEndReached срабатывает слишком часто.
Альтернативы и улучшения
- Cursor-based API: возвращайте nextCursor в ответе и запрашивайте по cursor, а не по номеру страницы. Это уменьшает проблемы, когда записи добавляются/удаляются между запросами.
- Кнопка “Загрузить ещё”: проще в плане UX и контроля сетевых запросов, удобна для экономии трафика.
- Предзагрузка следующей страницы: предзагрузка при достижении, например, 30% от конца — сокращает задержки при скролле.
- Кеширование: храните полученные страницы в локальном кеше (Redux, Zustand, React Query) для быстрой навигации.
- Оптимизация рендеринга: используйте getItemLayout, initialNumToRender, removeClippedSubviews для больших списков.
Обработка ошибок и стратегия повторов
- Уведомляйте пользователя о сетевых ошибках (Toast, Snackbar).
- Повторные попытки с экспоненциальным бэкоффом для временных сбоев.
- Для критичных списков отображайте контролы “Повторить” и возможность рестарта загрузки.
- Логируйте ошибки и метрики загрузки (Sentry, LogRocket) для анализа.
UX: когда выбирать infinite scroll vs Load More
- Infinite scroll: лучше для бесконечных лент, когда пользователь редко переходит к определённому месту.
- Load More button: лучше для детального контроля и когда важно предсказуемое поведение (например, каталог товаров с фильтрами).
Important: Для каталогов с фильтрами и сортировкой кнопка “Загрузить ещё” даёт пользователю возможность видеть изменения запроса и повторно запускать загрузку.
Критерии приёмки (acceptance)
- При первоначальной загрузке отображается первая страница данных.
- При достижении конца списка срабатывает подгрузка следующей страницы.
- При ошибке отображается сообщение об ошибке и кнопка повтора.
- Дубликаты элементов не появляются при повторных подгрузках.
- Когда достигнут последний элемент (totalPages или пустой ответ), подгрузка прекращается.
Ролевые чек-листы
Разработчик:
- Реализовать fetchBooks с обработкой ошибок.
- Обеспечить индикатор загрузки (spinner) и защиту от повторных запросов (isLoading).
- Добавить тесты для fetchMore и edge-cases.
QA:
- Проверить поведение при медленном соединении.
- Проверить сценарии смены фильтров и очистки списка.
- Проверить отсутствие дубликатов и правильный порядок элементов.
Product Manager:
- Утвердить, какой подход к пагинации нужен (page vs cursor).
- Решить UX: infinite scroll или Load More.
Тест-кейсы и приёмочные критерии
- TC1: Первичная загрузка — первая страница должна отображаться за разумное время.
- TC2: Подгрузка при прокрутке — при достижении конца список увеличивается.
- TC3: Повторный клик/быстрая прокрутка — isLoading предотвращает дублирование запросов.
- TC4: Ошибка сервера — приложение показывает сообщение и кнопку “Повторить”.
- TC5: Пустая страница/конец — больше запросов не выполняется.
Производительность: хитрости и лучшие практики
- Используйте keyExtractor с уникальным ключом (id).
- Ограничьте количество одновременно отрисовываемых элементов (initialNumToRender).
- Применяйте getItemLayout, если все элементы имеют одинаковую высоту — это ускорит прокрутку.
- Удаляйте отрисованные элементы вне видимости с removeClippedSubviews.
- Сокращайте вес сети: используйте поля select/partials в API, возвращайте только необходимые поля.
Безопасность и конфиденциальность
- Не включайте в запросы чувствительную информацию в URL-параметрах.
- Для приватных данных используйте авторизацию (Bearer токены) и HTTPS.
- Для GDPR/локальных законов: минимизируйте персональные данные в ответах API и хранении на устройстве.
Пример decision flow (Mermaid)
flowchart TD
A[Начало] --> B{Тип данных изменчив?}
B -- Да --> C[Использовать cursor-based API]
B -- Нет --> D[Использовать page-based API]
C --> E{UX: infinite scroll?}
D --> E
E -- Да --> F[Реализовать FlatList + onEndReached + индикатор]
E -- Нет --> G[Реализовать Load More кнопку и явную пагинацию]
F --> H[Добавить debounce и предзагрузку]
G --> H
H --> I[Кеширование и обработка ошибок]
I --> J[Готово]Шаблон плейбука для внедрения (SOP)
- Выбрать стратегию (page / cursor / offset).
- Спроектировать API-ответ (items, page/nextCursor, hasMore/totalPages).
- Реализовать утилиту fetch с единообразной обработкой ошибок.
- На клиенте — реализовать state: items, page/cursor, isLoading, error, hasMore.
- Подключить FlatList с onEndReached или кнопку “Загрузить ещё”.
- Добавить индикаторы загрузки и повтора.
- Написать unit и интеграционные тесты.
- Провести нагрузочное тестирование на устройстве и эмуляторе.
- Запустить A/B тест (при необходимости) и собрать метрики UX.
Глоссарий (1‑строчные определения)
- Пагинация: разбиение данных на порции.
- Page-based: пагинация по номеру страницы.
- Cursor-based: пагинация по указателю (cursor).
- Infinite scroll: автоматическая подгрузка при прокрутке.
- FlatList: компонент React Native для производительных списков.
Часто задаваемые вопросы (FAQ)
Как избежать дублирования элементов при подгрузке?
Проверяйте уникальность id перед добавлением в массив, либо используйте Set/Map для фильтрации.
Когда лучше использовать cursor-based пагинацию?
Когда данные часто обновляются или объём очень велик — cursor уменьшает проблемы с оффсетами.
Нужно ли показывать кнопку “Загрузить ещё” на мобильных устройствах?
Зависит от UX: кнопка полезна для экономии трафика и контроля пользователя; infinite scroll — для плавной ленты.
Короткий итог: кастомная пагинация в React Native — это комбинация корректно спроектированного API, надежной fetch-логики и продуманного UI. Выберите стратегию по требованиям и масштабируйте шагами: сначала простая реализация, затем оптимизация кеша и рендеринга.