Пагінація та Infinite Scroll
Пагінація та Infinite Scroll
Списки даних — це хліб і масло веб-розробки. Але завантажувати 10,000 записів одразу — погана ідея. Нам потрібна пагінація або "нескінченна прокрутка".
TanStack Query перетворює це складне завдання на задоволення.
Плавна Пагінація (Paginated Queries)
Припустимо, ми маємо звичайний запит:
const { data, isPending } = useQuery({
queryKey: ['projects', page],
queryFn: () => fetchProjects(page),
});
Проблема: Коли користувач натискає "Next Page", page змінюється з 1 на 2.
- Ключ змінюється
['projects', 1]->['projects', 2]. - Так як даних для сторінки 2 ще немає в кеші, запит переходить в стан
isPending: true. - Користувач бачить "Loading..." спіннер і список зникає. Це "смиканий" UX.
Рішення: Ми хочемо залишити дані сторінки 1 на екрані, поки завантажується сторінка 2.
У v5 для цього використовується placeholderData.
import { useQuery, keepPreviousData } from '@tanstack/react-query';
function ProjectsList() {
const [page, setPage] = useState(1);
const { data, isPending, isPlaceholderData } = useQuery({
queryKey: ['projects', page],
queryFn: () => fetchProjects(page),
// ✨ МАГІЯ ТУТ
placeholderData: keepPreviousData,
});
return (
<div>
{isPending ? (
<div>Loading...</div>
) : (
<div style={{ opacity: isPlaceholderData ? 0.5 : 1 }}>
{data.items.map(project => (
<div key={project.id}>{project.name}</div>
))}
</div>
)}
<button onClick={() => setPage(old => old - 1)} disabled={page === 1}>
Previous
</button>
<button onClick={() => setPage(old => old + 1)}>
Next
</button>
</div>
);
}
Як це працює?
- Коли ми переходимо на сторінку 2, Query не вмикає
isPending. - Замість цього, він показує дані зі сторінки 1 (Previous Data), але позначає їх як
isPlaceholderData: true. - У фоні вантажиться сторінка 2.
- Як тільки вона завантажиться, UI плавно оновлюється.
Prefetching (Попереднє Завантаження)
Щоб зробити UX ще кращим, ми можемо завантажити сторінку 2 заздалегідь, поки користувач ще дивиться на сторінку 1.
// В useEffect, коли змінюється сторінка
useEffect(() => {
if (!isPlaceholderData && data?.hasMore) {
queryClient.prefetchQuery({
queryKey: ['projects', page + 1],
queryFn: () => fetchProjects(page + 1),
});
}
}, [data, isPlaceholderData, page, queryClient]);
Тепер при натисканні "Next", дані з'являться миттєво, бо вони вже в кеші.
Infinite Scroll (useInfiniteQuery)
Пагінація "Завантажити ще" (як у Twitter/Instagram) вимагає іншого підходу. Нам потрібно не замінювати сторінку 1 на 2, а додавати сторінку 2 до сторінки 1.
Для цього є спеціальний хук useInfiniteQuery.
Структура API
Ваш бекенд повинен повертати щось на зразок курсора для наступної сторінки:
{
"items": [...],
"nextCursor": 20
}
Реалізація
import { useInfiniteQuery } from '@tanstack/react-query';
function InfiniteFeed() {
const {
data, // Структура змінилась!
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status,
} = useInfiniteQuery({
queryKey: ['feed'],
queryFn: ({ pageParam }) => fetchFeed(pageParam),
// Початковий курсор
initialPageParam: 0,
// Логіка отримання наступного курсора з відповіді бекенда
getNextPageParam: (lastPage, allPages) => {
// lastPage — це те, що повернув останній запит
return lastPage.nextCursor ?? undefined;
// Якщо повертаємо undefined, hasNextPage стане false
},
});
if (status === 'pending') return <div>Loading...</div>;
if (status === 'error') return <div>Error: {error.message}</div>;
return (
<div>
{/* data.pages — це масив масивів (масив сторінок) */}
{data.pages.map((group, i) => (
<React.Fragment key={i}>
{group.items.map((project) => (
<div key={project.id} className="card">
{project.name}
</div>
))}
</React.Fragment>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'Nothing more to load'}
</button>
<div>{isFetching && !isFetchingNextPage ? 'Background Updating...' : null}</div>
</div>
);
}
Важливі відмінності data
У useQuery data — це просто відповідь.
У useInfiniteQuery data — це об'єкт:
{
pages: [responsePage1, responsePage2, ...],
pageParams: [param1, param2, ...]
}
Тому нам завжди треба робити подвійний map (спочатку по сторінках, потім по елементах сторінки).
Bi-directional Infinite Scroll
v5 також підтримує двонаправлений скрол (вгору і вниз, як у месенджерах), використовуючи getPreviousPageParam.
Оптимістичні Оновлення: Швидше за Світло
Ви коли-небудь помічали, як працює лайк в Instagram або повідомлення в Telegram? Ви натискаєте кнопку, і серце стає червоним миттєво. Додаток не чекає, поки сервер скаже "ОК". Він оптимістично припускає, що все буде добре.
Просунуті Патерни та Оптимізація
Ви вже вмієте робити базові речі. Тепер перейдемо до технік "чорного поясу". Ці патерни допоможуть вирішити складні архітектурні завдання та оптимізувати продуктивність.