Гид по технологиям

Кастомная пагинация в React Native: динамическая подгрузка данных

6 min read Мобильная разработка Обновлено 08 Jan 2026
Кастомная пагинация в React Native
Кастомная пагинация в React Native

Код на ноутбуке, находящемся на земле

Пагинация — это система разделения большого объёма данных на части (страницы), чтобы упростить загрузку и отображение. Кастомная пагинация даёт разработчику гибкость: можно адаптировать UX, логику выборки и стратегию кеширования под конкретные требования приложения.

Что такое кастомная пагинация

Кастомная пагинация — это реализация механизма постраничной подгрузки данных, настроенного под специфику приложения. Это может быть классическая постраничная навигация, бесконечный скролл (infinite scroll), ленивый рендер (lazy loading) или гибридные решения. Важная цель — подгружать только необходимые данные и минимизировать задержки и расход памяти.

Краткое определение: пагинация — разбивка большого набора данных на управляемые блоки (страницы), которые загружаются по требованию.

Преимущества кастомной пагинации

  • Повышение масштабируемости: приложение обрабатывает большие наборы данных эффективнее.
  • Улучшение производительности: уменьшается объём данных, загружаемых и рендеримых одновременно.
  • Контроль UX: можно настроить поведение для мобильных сценариев, например индикацию загрузки и стратегию предзагрузки.

Важно: кастомная пагинация требует внимания к ошибкам, дедупликации данных и согласованности состояния при переключении сетевых условий.

Общая схема динамической подгрузки

При динамической подгрузке приложение запрашивает у сервера только ту страницу данных, которая необходима в текущий момент. Общие шаги:

  1. Выбрать метод пагинации (offset/page-based или cursor-based).
  2. Реализовать серверные эндпоинты, которые возвращают отдельные страницы/кусочки данных.
  3. На клиенте слушать действия пользователя: клик «Загрузить ещё», скролл до конца и т.п.
  4. Обновлять состояние и интерфейс после получения новых данных.

Пример ответа API

Предположим REST API возвращает список книг. Пример структуры ответа:

{  
  "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  
}  

Этот пример — классический offset/page-based ответ: сервер сообщает номер страницы и общее число страниц.

Функция выборки данных (fetch)

Ниже — простая функция fetchBooks, которая запрашивает данные по странице. В реальном проекте учтите таймауты, отмену запросов и повторные попытки.

const PAGE_SIZE = 10;  
  
const fetchBooks = async (page) => {  
  try {  
    const response = await fetch(`https://myapi.com/books?page=${page}&pageSize=${PAGE_SIZE}`);  
    const json = await response.json();  
    return json.data;  
  } catch (error) {  
    console.error(error);  
    return [];  
  }  
}  

Замечание: перед использованием fetch в продакшене полезно оборачивать вызов в слой, который обрабатывает заголовки авторизации, коды ошибок и нормализует ответ.

Реализация на стороне клиента: FlatList и onEndReached

FlatList — рекомендуемый компонент для длинных списков в React Native. Ниже — пример минимальной реализации с загрузкой следующей страницы при достижении конца списка.

import React, { useState, useEffect } from 'react';  
import { FlatList, View, Text } from 'react-native';  
  
const App = () => {  
  const [books, setBooks] = useState([]);  
  const [currentPage, setCurrentPage] = useState(1);  
  
  useEffect(() => {  
    // Fetch initial page of data  
    fetchBooks(currentPage).then(data => setBooks(data));  
  }, []);  
  
  const renderItem = ({ item }) => {  
    return (  
        
        {item.title}  
        {item.author}  
        
    );  
  };  
  
  return (  
     item.id.toString()}  
    />  
  );  
}  
  
export default App;  

Далее — расширение этой реализации, чтобы подгружать новые страницы по onEndReached.

import React, { useState, useEffect } from 'react';  
import { FlatList, View, Text } from 'react-native';  
  
const App = () => {  
  const [books, setBooks] = useState([]);  
  const [currentPage, setCurrentPage] = useState(1);  
  const [isLoading, setIsLoading] = useState(false);  
  
  useEffect(() => {  
    fetchBooks(currentPage).then(data => setBooks(data));  
  }, []);  
  
  const fetchMore = async () => {  
    if (isLoading) return;  
  
    setIsLoading(true);  
  
    const nextPage = currentPage + 1;  
    const newData = await fetchBooks(nextPage);  
  
    setCurrentPage(nextPage);  
    setIsLoading(false);  
    setBooks(prevData => [...prevData, ...newData]);  
  };  
  
  const renderItem = ({ item }) => {  
    return (  
        
        {item.title}  
        {item.author}  
        
    );  
  };  
  
  return (  
     item.id.toString()}  
      onEndReached={fetchMore}  
      onEndReachedThreshold={0.1}  
    />  
  );  
}  
  
export default App;  

Пояснения к параметрам:

  • isLoading — предотвращает одновременные параллельные вызовы.
  • onEndReachedThreshold — триггер сработает, когда пользователь приблизится на 10% к концу списка.

Паттерны пагинации: offset (page) vs cursor

  1. Offset / page-based (как в примерах выше): сервер принимает номер страницы и возвращает фиксированное количество записей.

    • Простая реализация.
    • Проблема: при изменении данных (вставка/удаление элементов) номера страниц могут «сдвигаться», приводя к дублированию или пропуску элементов.
  2. Cursor-based (cursor/seek): сервер возвращает маркер (cursor), который указывает точку продолжения. Клиент запрашивает следующую страницу, передавая cursor.

    • Более устойчив к изменениям набора данных (новые записи не ломают последовательность).
    • Требует дополнительной логики на сервере и валидации cursor.

Рекомендация: для лент (feed), реального времени или больших изменяющихся наборов используйте cursor-based. Для простых каталогов или при строгом контроле версий можно применять offset.

Пример cursor-based ответного тела (синтаксис для понимания):

{
  "data": [...],
  "nextCursor": "eyJpZCI6MTIzNDU2fQ==",
  "hasMore": true
}

Серверные советы и общая архитектура

  • Ограничьте PAGE_SIZE сверху на сервере, чтобы клиенты не запрашивали чрезмерно большие страницы.
  • Поддерживайте тайм-ауты и возможность отмены запросов (например, AbortController в браузерном fetch-слое).
  • Для cursor-based пагинации храните стабильные и детерминированные курсоры (на основе id, timestamp или составных ключей).
  • Документируйте границы и поведение эндпоинтов: что происходит при запросе несуществующей страницы, как возвращается пустой результат.

UX и взаимодействие с пользователем

UX — ключ к удобству. Несколько практик:

  • Показывайте индикаторы загрузки (footer spinner или skeleton-карточки).
  • Обрабатывайте состояние ошибки с возможностью повторить загрузку.
  • При использовании бесконечного скролла предлагайте кнопку «Вернуться к началу» для длинных лент.
  • Для каталогов с сортировкой/фильтрами сбрасывайте пагинацию при изменении параметров.

Важно: подумайте о доступности — элементы управления должны быть доступны для экранных читалок и навигации с клавиатуры (если релевантно).

Обработка ошибок, дедупликация и атомарность

  • Дедупликация: при мерджинге новых данных в список проверяйте уникальные id, чтобы избежать дублей.
  • Атомарность обновления: обновляйте state через функциональный setState(prev => …) чтобы избежать гонок.
  • Повторные попытки: задайте стратегию повторов (например, экспоненциальный бэкофф) для временных сетевых ошибок.

Пример простого мерджинга с дедупликацией:

setBooks(prev => {
  const ids = new Set(prev.map(b => b.id));
  const merged = [...prev, ...newData.filter(b => !ids.has(b.id))];
  return merged;
});

Тестирование и критерии приёмки

Критерии приёмки:

  • При первой загрузке отображается первая страница и индикатор загрузки.
  • При скролле до конца вызывается fetchMore и новые элементы добавляются в список.
  • Повторные вызовы при активной загрузке не выполняются (isLoading блокирует).
  • При ошибке отображается сообщение и возможность повтора.
  • При изменении фильтров/сортировки список сбрасывается и загружается заново.

Тест-кейсы:

  • Загрузка первой страницы при нормальном ответе.
  • Поведение при пустом ответе (data = []).
  • Обработка сетевой ошибки и успешный повтор.
  • Эмуляция вставки элемента на сервере между запросами (для offset-based) — проверить отсутствие пропусков/дублей.

Role-based чек-листы

Для разработчика:

  • Реализовать fetch слой с таймаутом/отменой.
  • Добавить isLoading и обработку ошибок.
  • Реализовать дедупликацию и локальный кеш.

Для тестировщика:

  • Написать unit-тесты для логики мерджинга и error handling.
  • Проверить нагрузку с эмуляцией медленной сети.

Для продакшен-инженера:

  • Ограничить PAGE_SIZE на сервере и мониторить SLO по latency.
  • Настроить логирование ошибок запросов и метрики успешных/неуспешных вызовов.

Альтернативные подходы и когда кастомная пагинация не нужна

Альтернативы:

  • Серверная агрегация и предвыборка (если доступный объём позволяет) — всё сразу и кэшировать на CDN.
  • GraphQL с cursor-based пагинацией (Relay-style cursors).

Когда кастомная пагинация может не подходить:

  • Если набор данных небольшой и помещается в память клиента.
  • При требованиях «консистентной снимки» больших наборов данных, где нужна транзакционная целостность (тогда лучше серверная подготовка представления).

Производительность и оптимизации

  • Предзагрузка следующей страницы: запуск fetch за N процентов до конца, чтобы сократить видимые задержки.
  • Снижение веса элементов: уменьшайте вложенность View и рендерите минимально необходимую структуру.
  • Использование keyExtractor и getItemLayout (если элементы фиксированной высоты) для оптимизации скролла.
  • Кеширование на уровне клиента (в памяти или с использованием локального хранилища) для повторных просмотров.

Безопасность и приватность

  • Не включайте в cursor чувствительные данные в явном виде. Если курсор содержит сериализованные поля, шифруйте их или используйте HMAC.
  • Для персональных данных соблюдайте требования локального законодательства (например, GDPR) — возвращайте только те поля, на которые у пользователя есть права.

Миграция: от offset к cursor

Переходная стратегия:

  1. Внедрите cursor-эндпоинт параллельно с существующим offset.
  2. На клиенте внедрите feature-flag для поэтапного переключения пользователей.
  3. Проверяйте согласованность данных и ведите мониторинг ошибок.

Шаблон SOP для добавления пагинации в новый экран

  1. Описать требования UX и сценарии (infinite scroll vs button).
  2. Определить тип пагинации (offset/cursor).
  3. Реализовать серверный контракт и описать структуру ответа.
  4. Написать fetch-обёртку с таймаутами и retry.
  5. Реализовать компонент списка, индикаторы загрузки и обработку ошибок.
  6. Добавить unit-тесты и e2e сценарии.
  7. Произвести нагрузочное тестирование на сервере.
  8. Выпустить фичу за канареечным развертыванием и собирать метрики.

Краткое резюме

Кастомная пагинация даёт контроль и гибкость при работе с большими списками в мобильных приложениях. Выбор между offset и cursor зависит от поведения данных и требований к консистентности. Обратите внимание на UX-индикаторы, обработку ошибок, дедупликацию и производительность.

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

Поделиться: X/Twitter Facebook LinkedIn Telegram
Автор
Редакция

Похожие материалы

Градиенты в Canva: добавить и настроить
Дизайн

Градиенты в Canva: добавить и настроить

Ошибка Disabled accounts can't be contacted в Instagram
Социальные сети

Ошибка Disabled accounts can't be contacted в Instagram

Генерация случайных чисел в Google Sheets
Google Таблицы

Генерация случайных чисел в Google Sheets

Прокручиваемые скриншоты в Windows 11
Windows

Прокручиваемые скриншоты в Windows 11

Как установить корпусной вентилятор в ПК
Железо

Как установить корпусной вентилятор в ПК

Check In в iOS 17: настройка и безопасность
How-to

Check In в iOS 17: настройка и безопасность