Перейти к содержимому

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,
})

Самая важная опция — функция, определяющая параметр для следующей страницы. Возвращает 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) ?? []

Кнопка:

<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: (firstPage) => firstPage.prevCursor ?? undefined