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

Пагинация и бесконечная прокрутка с TanStack Query в Next.js

7 min read Frontend Обновлено 26 Apr 2026
Пагинация и бесконечная прокрутка — TanStack Query
Пагинация и бесконечная прокрутка — TanStack Query

Экран ноутбука с кодом и органайзером с ручками рядом.

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

В этой статье вы найдёте:

  • понятия и мета-уровневые рекомендации;
  • рабочие примеры для Next.js + TanStack Query;
  • альтернативы, ограничения и сценарии, когда стоит выбрать другой путь;
  • практические чеклисты, критерии приёмки и вопросы для QA.

Почему пагинация и бесконечная прокрутка важны

Определения:

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

Ментальная модель: представьте огромную книгу. Пагинация — листы по главам, пользователь выбирает страницу. Бесконечная прокрутка — книга, в которую автоматически подшивают новые листы по мере чтения.

Ключевые плюсы/минусы в одном предложении:

  • Пагинация — хороша для навигации, совместима с SEO и даёт предсказуемую нагрузку на сервер.
  • Бесконечная прокрутка — удобна для «ленты» контента, но хуже для навигации, закладок и SEO.

Когда что выбрать (эвристика):

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

Что такое TanStack Query

TanStack Query — это библиотека для управления асинхронными запросами и кешем в приложениях на React/Next.js. Коротко: упрощает fetch/кеш/повторные попытки/статусы (isLoading, isError и т.д.).

Краткое определение терминов:

  • queryKey — уникальный ключ запроса; влияет на инвалидацию и кеш.
  • queryFn — функция, которая возвращает промис с данными.
  • staleTime / cacheTime — настройки времени жизни кеша.

Ноутбук с открытым кодом на рабочем столе.

Установка и начальная настройка Next.js

Создайте проект Next.js (App Router в Next.js 13+):

npx create-next-app@latest next-project --app

Установите TanStack Query:

npm i @tanstack/react-query

Запомните: для демонстрации и локальной разработки используйте тестовые API или собственный бекенд с пагинацией по параметрам _page и _limit (или аналоги).

Интеграция TanStack Query в Next.js

В корне приложения (файл layout.js) инициализируйте QueryClient и оберните провайдером все дочерние компоненты, чтобы иметь доступ к кэшу на уровне приложения.

"use client"  
import React from 'react'  
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';  
  
const metadata = {  
  title: 'Create Next App',  
  description: 'Generated by create next app',  
};  
  
export default function RootLayout({ children }) {  
  const queryClient = new QueryClient();  
  
  return (  
      
        
          
          {children}  
          
        
      
  );  
}  
  
export { metadata };

Важно: QueryClient можно настраивать (defaultOptions) для retry, staleTime и т.д. Это влияет на поведение всего приложения.

Реализация пагинации с useQuery

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

Создайте файл src/app/Pagination/page.js и вставьте базовый набор импортов:

"use client"  
import React, { useState } from 'react';  
import { useQuery} from '@tanstack/react-query';  
import './page.styles.css';

Далее функция для загрузки постов (в примере используется JSONPlaceholder):

export default function Pagination() {  
  const [page, setPage] = useState(1);  
  
  const fetchPosts = async () => {  
    try {  
      const response = await fetch(`https://jsonplaceholder.typicode.com/posts?  
                          _page=${page}&_limit=10`);  
  
      if (!response.ok) {  
        throw new Error('Failed to fetch posts');  
      }  
  
      const data = await response.json();  
      return data;  
    } catch (error) {  
      console.error(error);  
      throw error;  
    }  
  };  
  
  // add the following code here  
}

Определяем useQuery с важной опцией keepPreviousData:

  const { isLoading, isError, error, data } = useQuery({  
    keepPreviousData: true,  
    queryKey: ['posts', page],  
    queryFn: fetchPosts,  
  });

Разъяснения и рекомендации:

  • keepPreviousData: true предотвращает «мигание» списка при переключении страниц, отображая старые данные, пока подгружаются новые.
  • Альтернатива: вместо keepPreviousData часто используют staleTime, чтобы считать данные актуальными в течение X миллисекунд и избегать лишних запросов.
  • queryKey важно формировать консистентно: [‘posts’, page, filter, sort] — включайте все параметры, влияющие на результат.

Обработка состояний:

  if (isLoading) {  
    return (

Loading...

); } if (isError) { return ({error.message}); }

Отображение списка и навигация:

  return (  
    

Next.js Pagination

{data && (
    {data.map((post) => (
  • {post.title}
  • ))}
)}
);

Примечание по практике: в примере используется Math.max(prev - 1, 0), что допускает страницу 0. Обычно счёт страниц начинается с 1 — используйте Math.max(prev - 1, 1), если ваш API не поддерживает 0.

Рекомендации по производительности и UX:

  • Устанавливайте разумный _limit (10–50) в зависимости от размера записей.
  • Используйте staleTime, чтобы уменьшить количество запросов при частых переключениях страниц.
  • Для крупных изображений применяйте ленивую загрузку (lazy loading).

Запуск локального сервера:

npm run dev

Откройте http://localhost:3000/Pagination

Пример пагинации TanStack Query в приложении Next.js.

Поскольку папка Pagination находится в app, Next.js автоматически создаёт маршрут /Pagination.

Бесконечная прокрутка с useInfiniteQuery

useInfiniteQuery упрощает подгрузку страниц по мере прокрутки. Подходит для «лент» и галерей.

Создайте src/app/InfiniteScroll/page.js и вставьте следующие импорты:

"use client"  
import React, { useRef, useEffect, useState } from 'react';  
import { useInfiniteQuery } from '@tanstack/react-query';  
import './page.styles.css';

Функция fetchPosts с pageParam и искусственной задержкой (в примере для видимой анимации):

export default function InfiniteScroll() {  
  const listRef = useRef(null);  
  const [isLoadingMore, setIsLoadingMore] = useState(false);  
  
  const fetchPosts = async ({ pageParam = 1 }) => {  
    try {  
      const response = await fetch(`https://jsonplaceholder.typicode.com/posts?  
                          _page=${pageParam}&_limit=5`);  
  
      if (!response.ok) {  
        throw new Error('Failed to fetch posts');  
      }  
  
      const data = await response.json();  
      await new Promise((resolve) => setTimeout(resolve, 2000));  
      return data;  
    } catch (error) {  
      console.error(error);  
      throw error;  
    }  
  };  
  
  // add the following code here  
}

Определение useInfiniteQuery:

  const { data, fetchNextPage,  hasNextPage, isFetching } = useInfiniteQuery({  
    queryKey: ['posts'],  
    queryFn: fetchPosts,  
    getNextPageParam: (lastPage, allPages) => {  
      if (lastPage.length < 5) {  
        return undefined;  
      }  
      return allPages.length + 1;  
    },  
  });  
  
  const posts = data ? data.pages.flatMap((page) => page) : [];

Объяснение: getNextPageParam должен вернуть undefined, если страниц больше нет; иначе возвращайте номер следующей страницы.

Intersection Observer для подгрузки при пересечении:

  const handleIntersection = (entries) => {  
    if (entries[0].isIntersecting && hasNextPage && !isFetching && !isLoadingMore) {  
      setIsLoadingMore(true);  
      fetchNextPage();  
    }  
  };  
  
  useEffect(() => {  
    const observer = new IntersectionObserver(handleIntersection, { threshold: 0.1 });  
  
    if (listRef.current) {  
      observer.observe(listRef.current);  
    }  
  
    return () => {  
      if (listRef.current) {  
        observer.unobserve(listRef.current);  
      }  
    };  
  }, [listRef, handleIntersection]);  
  
  useEffect(() => {  
    if (!isFetching) {  
      setIsLoadingMore(false);  
    }  
  }, [isFetching]);

JSX для отображения списка:

  return (  
    

Infinite Scroll

    {posts.map((post) => (
  • {post.title}
  • ))}
{isFetching ? 'Fetching...' : isLoadingMore ? 'Loading more...' : null}
);

Откройте http://localhost:3000/InfiniteScroll

Когда один подход лучше другого — решение и критерии

Mermaid диаграмма для быстрой ориентации (копируйте в редактор, поддерживающий Mermaid):

flowchart TD
  A[Есть ли необходимость в SEO и прямой навигации?] -->|Да| B[Пагинация]
  A -->|Нет| C[Пользователь ожидает ленты или бесконечной загрузки]
  C --> D[Бесконечная прокрутка]
  B --> E[Хорошо для фильтров, закладок, аналитики]
  D --> F[Хорошо для вовлечения, непрерывного потребления контента]

Краткие эвристики:

  • SEO/индексация/фиксированные URL → Пагинация.
  • Непрерывное потребление, низкие требования к точной навигации → Бесконечная прокрутка.
  • Смешанные сценарии: подумайте о гибриде — пагинация для основных маршрутов и бесконечной загрузке в модальном окне или отдельной «ленте».

Альтернативные подходы и паттерны

  • Cursor-based pagination (пагинация по курсору): лучше для огромных наборов данных и для предотвращения дублирования/пропуска при параллельных вставках в базу.
  • Server-side rendering (SSR) для первой страницы + клиентская подгрузка остальных страниц для SEO-совместимости.
  • Static generation + revalidation (ISR) для контента, который редко меняется.

Контрпримеры — когда эти техники не подойдут

  • Очень маленькие наборы данных (до одной страницы) — лишняя сложность.
  • Строго транзакционные данные/реaltime (финансовые системы) — нужно WebSocket/пуш-обновления.
  • Сценарии, где пользователю необходимо вернуться к точному месту в списке с сохранением контекста (лучше пагинация или сохранение позиции на клиенте).

Тесты, критерии приёмки и QA

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

  • Страница пагинации корректно запрашивает и отображает данные для страниц 1..N.
  • Кнопка «Prev» отключена на первой странице; «Next» отключена при отсутствии следующей страницы.
  • Для бесконечной прокрутки: при прокрутке до конца добавляются новые записи; индикатор загрузки отображается.
  • Обработка ошибок: при недоступности API показывается понятное сообщение и возможность повтора.

Примеры тест-кейсов:

  • Переключение страниц: загрузка 1->2->1 без мерцания содержимого.
  • Бесконечная прокрутка: подгрузка до конца, затем отсутствие новых данных.
  • Сетевые ошибки: эмуляция 500 — приложение показывает сообщение и кнопки повтора.

Чеклисты по ролям

Разработчик:

  • Настроен QueryClient в layout.js
  • Корректный queryKey для всех параметров
  • Опции keepPreviousData/staleTime настроены
  • Обработаны loading/error состояния

QA:

  • Тесты на навигацию и подгрузку
  • Тесты на доступность (клавиатура, фокус)
  • Тесты производительности (время первого рендера, время отклика)

Product / PM:

  • Решение соответствует требованиям UX и метрикам (retention vs discoverability)
  • Определены границы контента для пагинации/ленты

Инфраструктура:

  • API поддерживает пагинацию (page/limit или cursor)
  • Логи и мониторинг на ошибках и таймаутах

Доступность и UX

  • Кнопки пагинации должны быть доступны для клавиатуры и иметь aria-label.
  • Для бесконечной прокрутки добавьте «Загрузить ещё» кнопку как альтернативу для пользователей клавиатуры/скринридеров.
  • Обязательно показывайте индикатор загрузки и текст состояния.

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

  • Не кешируйте приватные данные в общем QueryClient без проверки прав доступа.
  • Для персонализованного контента используйте уникальные queryKey, включающие идентификатор пользователя.

Мини‑методология развертывания (пошагово)

  1. Проанализируйте пользовательские сценарии и требования SEO.
  2. Выберите стратегию (pagination / infinite / hybrid).
  3. Настройте QueryClient и добавьте глобальные опции (staleTime, retry).
  4. Имплементируйте fetch функции с правильной пагинацией на сервере (page/limit или cursor).
  5. Обработайте состояния loading/error/empty.
  6. Напишите тесты для всевозможных сценариев (пагинация, прокрутка, ошибки).
  7. Деплой и мониторинг метрик (latency, error rate).

Часто задаваемые вопросы

Можно ли использовать useQuery вместо useInfiniteQuery для бесконечной прокрутки?

Да, технически можно, но useInfiniteQuery предоставляет удобный API для работы со страницами и их агрегации. useQuery потребует больше ручной логики и кода.

Как выбрать размер страницы (_limit)?

Рекомендуется ориентироваться на вес записи и UX: для простых текстовых списков 10–20 записей, для медиа — меньше. Это эвристика: тестируйте производительность.

Как избежать дублирования при параллельных вставках в базу?

Используйте cursor-based pagination (cursor вместо номера страницы). Это надёжнее при динамическом изменении источника данных.

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

  • Пагинация и бесконечная прокрутка решают проблемы с производительностью при больших объёмах данных.
  • TanStack Query упрощает реализацию обоих подходов благодаря кешу и спец‑хукам (useQuery, useInfiniteQuery).
  • Выбор стратегии зависит от требований UX, SEO и характера данных.

Important: тестируйте на реальных объёмах данных, чтобы подобрать оптимальные лимиты и параметры кеширования.

Конец статьи.

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

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

Цикл ходьбы: как анимировать пошагово
Анимация

Цикл ходьбы: как анимировать пошагово

Создать логотип в Photoshop: формы и эксперименты
Графический Дизайн

Создать логотип в Photoshop: формы и эксперименты

Первый Android: настройка и базовое использование
Mobile

Первый Android: настройка и базовое использование

Скриншоты в Windows 11 — Snip and Sketch
Windows

Скриншоты в Windows 11 — Snip and Sketch

Artfol — соцсеть для художников: обзор и руководство
Искусство

Artfol — соцсеть для художников: обзор и руководство

Профессиональная подпись email в Canva
Дизайн электронной почты

Профессиональная подпись email в Canva