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

Большинство приложений работают с данными, и по мере роста приложений объём данных может становиться значительным. Если приложение не умеет эффективно обрабатывать большой объём данных, оно начнёт тормозить и выдавать плохой UX.
Пагинация и бесконечная прокрутка — два распространённых приёма, которые помогают оптимизировать скорость рендеринга и улучшить взаимодействие с пользователем. В этой статье мы рассмотрим, как реализовать оба подхода с помощью TanStack Query в Next.js (App directory, Next 13+).
Важно: в примерах используется публичный JSONPlaceholder как источник данных. В реальном приложении заменяйте URL на ваш API и учитывайте авторизацию, лимиты и кэш-политику.
Что такое TanStack Query (одно предложение)
TanStack Query — это библиотека управления асинхронными данными (кэширование, повторные запросы, обновление в фоне) для React-приложений. Она упрощает работу с запросами и состоянием данных.
Что такое пагинация и бесконечная прокрутка
- Пагинация: данные делятся на страницы. Пользователь перемещается между страницами вручную — обычно кнопками или нумерацией.
- Бесконечная прокрутка: данные подгружаются автоматически по мере прокрутки вниз. Подходит для лент и контента, где последовательность просмотра важнее точной навигации.
Ключевой выбор зависит от задачи: удобство навигации (пагинация) против бесшовного просмотра (infinite scroll).
Репозиторий с кодом
Исходный код примеров доступен в репозитории проекта (ссылка в оригинале). Используйте его как отправную точку.
Установка и настройка Next.js (App directory)
Чтобы начать, создайте проект Next.js с App directory:
npx create-next-app@latest next-project --appЗатем установите TanStack Query (React Query):
npm i @tanstack/react-queryИнтеграция TanStack Query в Next.js
Создайте и инициализируйте QueryClient в корневом layout приложения (обычно файл src/app/layout.js или src/app/layout.tsx). Оборачивайте children в QueryClientProvider.
"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 };Эта настройка даёт доступ к TanStack Query в любом компоненте приложения.
Пагинация с useQuery — пошаговое руководство
В этой секции мы реализуем страницу Pagination с использованием хука 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 и параметры:
const { isLoading, isError, error, data } = useQuery({
keepPreviousData: true,
queryKey: ['posts', page],
queryFn: fetchPosts,
});Объяснение параметров:
- keepPreviousData: true — при переходе на другую страницу показывать предыдущие данные пока подгружаются новые. Это даёт менее дерганый UX.
- queryKey: [‘posts’, page] — ключ привязан к странице, поэтому кэш хранит версии по каждой странице.
- queryFn: функция, которая делает fetch.
Обработка состояний загрузки и ошибок:
if (isLoading) {
return (Loading...
);
}
if (isError) {
return ({error.message}
);
}Рендер UI и кнопки навигации:
return (
Next.js Pagination
{data && (
{data.map((post) => (
- {post.title}
))}
)}
);Запустите сервер разработки:
npm run devОткройте: http://localhost:3000/Pagination
Папка Pagination в app-directory автоматически становится маршрутом.
Бесконечная прокрутка с useInfiniteQuery — пошагово
Интересный UX-пример — лента, похожая на YouTube: новые элементы подгружаются автоматически.
Создайте src/app/InfiniteScroll/page.js и подключите стили.
"use client"
import React, { useRef, useEffect, useState } from 'react';
import { useInfiniteQuery } from '@tanstack/react-query';
import './page.styles.css';Функция fetch и компонент:
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 и формируем flat-список постов из страниц:
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) : [];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
Сравнение: когда выбирать пагинацию, а когда бесконечную прокрутку
- Если пользователям важна возможность сохранить конкретную страницу и вернуться к ней — выбирайте пагинацию.
- Если важно быстро показывать поток контента (ленты, социальные стены) — удобнее бесконечная прокрутка.
- Пагинация легче индексируется поисковиками и лучше подходит для SEO.
- Бесконечная прокрутка требует внимательного подхода к доступности и управлению памятью на клиенте.
Альтернативы и расширения подхода
- Cursor-based pagination (позиционная/стримовая пагинация) — лучше для больших данных и для одновременных вставок/удалений в источнике.
- Server-side pagination — отдача уже разделённых страниц с сервера (иногда быстрее и экономичнее по сети).
- Комбинация: клиентская пагинация поверх cursor-пагинации API.
Практические советы по оптимизации и кэшированию
- Настройте staleTime и cacheTime в QueryClient, чтобы избежать лишних запросов.
- Используйте keepPreviousData при переходе между страницами для плавного UX.
- Для бесконечной прокрутки контролируйте объём DOM: удаляйте или виртуализируйте старые элементы (react-window, react-virtualized) при тысячах элементов.
- Ограничивайте количество параллельных fetch-запросов.
- В продакшне настойчиво рекомендуем включать HTTP-кэширование и ETag/If-None-Match на стороне API.
Доступность и UX
- Для пагинации предоставляйте явные кнопки с понятными aria-label и состояниями disabled.
- Для бесконечной прокрутки реализуйте «Загрузить ещё» как fallback для тех, у кого отключён JS или для вспомогательных технологий.
- Сообщайте пользователю состояние загрузки (прогресс, индикатор «подгружаю»).
Безопасность и приватность
- Не храните чувствительные данные в кэше клиента дольше, чем нужно.
- При работе с персональными данными учитывайте GDPR: минимизируйте передачу лишней информации, обезличивайте при необходимости.
- Используйте защищённые запросы (HTTPS) и токены доступа. Не логируйте токены или персональную информацию в клиентских ошибках.
Критерии приёмки
- Компонент пагинации корректно подгружает страницы при кликах и не теряет состояния страницы.
- Компонент бесконечной прокрутки подгружает следующие страницы при скролле и корректно показывает индикаторы загрузки.
- При плохом соединении UI показывает понятный текст ошибки и возможность повторить запрос.
- Производительность: рендер одной страницы не занимает более приемлемого времени (зависит от проекта), нет утечек памяти при длительном использовании ленты.
Чек-листы для ролей (разработчик / QA / PM)
Разработчик:
- Реализовал кэширование и handling ошибок.
- Добавил ограничения по количеству элементов в DOM или внедрил виртуализацию.
- Написал unit/integ тесты для ключевой логики загрузки страниц.
QA:
- Проверил переходы между страницами при разных задержках сети.
- Проверил поведение при отсутствии сети и при респонсах с ошибкой.
- Оценил память клиента при длительном использовании ленты.
PM (Product Manager):
- Оценил метрики успеха: время до первого взаимодействия, средняя глубина просмотра.
- Утвердил UX для мобильных и десктопных устройств.
Тест-кейсы и приёмочные критерии
- Загрузка первой страницы: отображается список и нет ошибок.
- Навигация «Prev/Next»: корректно переключается, кнопки disabled в нужных состояниях.
- Бесконечный скролл: при достижении низа вызывается fetchNextPage.
- Ошибки API: отображается сообщение об ошибке и кнопка «повторить».
- Производительность: после 1000 элементов в ленте приложение остаётся отзывчивым (или виртуализация включена).
Сценарии, когда подход может не подойти (контрпримеры)
- Если пользователь должен ссылаться на конкретную запись через URL (например, каталоги или архивы), бесконечная прокрутка мешает навигации.
- Если требуется точная нумерация результатов и их подсчёт — предпочтительна пагинация на сервере.
Ментальные модели и эвристики
- Модель «окно»: представьте, что пользователь видит только окно из N элементов. Подгружайте следующую «страницу», когда окно приближается к нижней границе.
- Эвристика «дежурного времени»: если загрузка новой страницы занимает меньше 300–500 мс, пользователь обычно не заметит лаг; если дольше — показывайте прогресс.
Миграция и совместимость
- При миграции с React Query v3 на TanStack Query (v4+) обратите внимание на изменение API (переименование пакета и некоторые опции). Тестируйте ключи запросов и дедупликацию.
- Для SSR/SSG учитывайте, что App directory в Next.js имеет особенности рендеринга; TanStack Query можно использовать и на сервере, но требуется дополнительная логика для гидратации.
Краткая методология внедрения (mini-playbook)
- Определите требования UX и SEO.
- Выберите стратегию пагинации (offset vs cursor).
- Настройте QueryClient и дефолтные опции (staleTime, retry, cacheTime).
- Реализуйте базовый компонент и покрытие тестами.
- Добавьте обработку ошибок, индикаторы загрузки и метрики.
- Проведите нагрузочное тестирование и профилирование на целевых устройствах.
Короткий глоссарий (1 строка на термин)
- Pagination: деление результата на страницы.
- Infinite scroll: автоматическая подгрузка данных при прокрутке.
- Cursor-based pagination: пагинация по курсору/позиции, устойчива к вставкам.
- keepPreviousData: опция TanStack Query, сохраняющая старые данные во время фетча.
Социальный превью (рекомендации)
OG title: Пагинация и бесконечная прокрутка с TanStack Query OG description: Пошаговое руководство по useQuery и useInfiniteQuery в Next.js, с чеклистами, тестами и оптимизациями.
Резюме
- TanStack Query помогает упростить работу с пагинацией и бесконечной прокруткой.
- Пагинация лучше для навигации и SEO; бесконечная прокрутка — для лент и бесшовного просмотра.
- Важны кэширование, управление памятью, доступность и безопасность при работе с большими данными.
Дополнительные ресурсы: документация TanStack Query, статьи по cursor-based pagination, библиотеки виртуализации (react-window).
Похожие материалы
RDP: полный гид по настройке и безопасности
Android как клавиатура и трекпад для Windows
Советы и приёмы для работы с PDF
Calibration в Lightroom Classic: как и когда использовать
Отключить Siri Suggestions на iPhone