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

Большинство приложений оперируют данными. По мере масштабирования объём данных растёт, и без правильной стратегии отображения приложение начинает тормозить. Пагинация и бесконечная прокрутка — два распространённых подхода для оптимизации производительности при отображении больших списков.
В этой статье вы найдёте:
- понятия и мета-уровневые рекомендации;
- рабочие примеры для 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

Поскольку папка 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, включающие идентификатор пользователя.
Мини‑методология развертывания (пошагово)
- Проанализируйте пользовательские сценарии и требования SEO.
- Выберите стратегию (pagination / infinite / hybrid).
- Настройте QueryClient и добавьте глобальные опции (staleTime, retry).
- Имплементируйте fetch функции с правильной пагинацией на сервере (page/limit или cursor).
- Обработайте состояния loading/error/empty.
- Напишите тесты для всевозможных сценариев (пагинация, прокрутка, ошибки).
- Деплой и мониторинг метрик (latency, error rate).
Часто задаваемые вопросы
Можно ли использовать useQuery вместо useInfiniteQuery для бесконечной прокрутки?
Да, технически можно, но useInfiniteQuery предоставляет удобный API для работы со страницами и их агрегации. useQuery потребует больше ручной логики и кода.
Как выбрать размер страницы (_limit)?
Рекомендуется ориентироваться на вес записи и UX: для простых текстовых списков 10–20 записей, для медиа — меньше. Это эвристика: тестируйте производительность.
Как избежать дублирования при параллельных вставках в базу?
Используйте cursor-based pagination (cursor вместо номера страницы). Это надёжнее при динамическом изменении источника данных.
Краткое резюме
- Пагинация и бесконечная прокрутка решают проблемы с производительностью при больших объёмах данных.
- TanStack Query упрощает реализацию обоих подходов благодаря кешу и спец‑хукам (useQuery, useInfiniteQuery).
- Выбор стратегии зависит от требований UX, SEO и характера данных.
Important: тестируйте на реальных объёмах данных, чтобы подобрать оптимальные лимиты и параметры кеширования.
Конец статьи.
Похожие материалы
Цикл ходьбы: как анимировать пошагово
Создать логотип в Photoshop: формы и эксперименты
Первый Android: настройка и базовое использование
Скриншоты в Windows 11 — Snip and Sketch
Artfol — соцсеть для художников: обзор и руководство