7. Query: бесконечная прокрутка
Что такое бесконечный запрос?
Заголовок раздела «Что такое бесконечный запрос?»useInfiniteQuery — специализированный хук для загрузки данных постранично с возможностью
подгрузки следующих страниц. Идеален для лент новостей, чатов, бесконечных списков.
Структура данных
Заголовок раздела «Структура данных»В отличие от useQuery, useInfiniteQuery возвращает объект data с полем pages —
массивом всех загруженных страниц:
data = { pages: [ { items: [...], nextCursor: 'cursor_1' }, // страница 1 { items: [...], nextCursor: 'cursor_2' }, // страница 2 { items: [...], nextCursor: null }, // страница 3 (последняя) ], pageParams: [undefined, 'cursor_1', 'cursor_2'],}Базовый пример
Заголовок раздела «Базовый пример»const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status,} = useInfiniteQuery({ queryKey: ['posts'], queryFn: ({ pageParam }) => fetchPosts({ cursor: pageParam, limit: 10 }), initialPageParam: undefined, getNextPageParam: (lastPage) => lastPage.nextCursor,})getNextPageParam
Заголовок раздела «getNextPageParam»Самая важная опция — функция, определяющая параметр для следующей страницы.
Возвращает undefined или null если страниц больше нет:
getNextPageParam: (lastPage, allPages) => { // Курсорная пагинация return lastPage.nextCursor ?? undefined
// Страничная пагинация return lastPage.hasMore ? allPages.length + 1 : undefined
// По offset return lastPage.items.length < 10 ? undefined : allPages.length * 10}Плоский список из всех страниц
Заголовок раздела «Плоский список из всех страниц»Для рендеринга обычно нужен плоский массив элементов:
const allPosts = data?.pages.flatMap(page => page.items) ?? []Кнопка “Загрузить ещё” vs Infinite Scroll
Заголовок раздела «Кнопка “Загрузить ещё” vs Infinite Scroll»Кнопка:
<button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage}> {isFetchingNextPage ? 'Загрузка...' : hasNextPage ? 'Загрузить ещё' : 'Всё загружено'}</button>Автоматически при скролле (через IntersectionObserver):
const observerRef = useRef<HTMLDivElement>(null)
useEffect(() => { const observer = new IntersectionObserver(entries => { if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) { fetchNextPage() } }) if (observerRef.current) observer.observe(observerRef.current) return () => observer.disconnect()}, [hasNextPage, isFetchingNextPage, fetchNextPage])getPreviousPageParam
Заголовок раздела «getPreviousPageParam»Для двунаправленной бесконечной прокрутки (например, чаты, загрузка истории вверх):
getPreviousPageParam: (firstPage) => firstPage.prevCursor ?? undefined