TL;DR
React Query + Intersection Observer — мощная комбинация для infinite scroll, но требует правильной настройки getNextPageParam. Разбираем как избежать типичных ошибок с пагинацией и оптимизировать запросы.
Введение: когда классическая пагинация не катит
В современных SPA бесконечный скролл стал де-факто стандартом для лент контента. Но его реализация часто превращается в ад из:
- Грязного стейта
- Race conditions
- Дублирующихся запросов
React Query с его useInfiniteQuery обещает избавить нас от этих проблем, но как показывает пример из Reddit — без понимания механики можно настрелять себе в ногу.
Разбираем проблемный код
Исходный пример содержит несколько критических ошибок:
// ❌ Проблемный момент 1: page из Redux
export const useFetch = () => {
const {page} = useAppSelector((state)=> state.page)
const {data, isLoading, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey:['data',page], // ❌ Зависимость от внешнего стейта
queryFn: () => getImages(),
getNextPageParam:(last_page) => last_page.page // ❌ Не учитывает next_page
})
return {data, isLoading, fetchNextPage, hasNextPage}
}
Что не так:
- Избыточная зависимость от Redux: useInfiniteQuery уже имеет внутренний механизм пагинации
- Неправильный getNextPageParam: просто возвращая текущую страницу, мы ломаем механизм подгрузки
- Отсутствие обработки next_page: API явно предоставляет URL следующей страницы, но мы его игнорируем
Правильная реализация
Перепишем хук с учетом best practices:
export const useFetchImages = () => {
return useInfiniteQuery({
queryKey: ['images'],
queryFn: ({ pageParam = 1 }) => getImages(pageParam),
getNextPageParam: (lastPage) => {
// Используем next_page из API или вычисляем следующую страницу
return lastPage.next_page
? extractPageFromUrl(lastPage.next_page)
: lastPage.page + 1
},
staleTime: 5 * 60 * 1000, // 5 минут
getPreviousPageParam: (firstPage) => firstPage.page - 1,
})
}
// Хелпер для извлечения page из URL
const extractPageFromUrl = (url: string) => {
const match = url.match(/page=(\d+)/)
return match ? parseInt(match[1]) : undefined
}
Ключевые изменения:
- Автономность: хук больше не зависит от внешнего стейта
- Полная интеграция с API: используем next_page если он есть
- Добавлен staleTime: предотвращаем избыточные запросы
- Поддержка双向 навигации: getPreviousPageParam для полного контроля
Оптимизируем Intersection Observer
Оригинальная реализация useBottomIndicator имеет проблемы с memory leaks:
const useScrollTrigger = () => {
const ref = useRef<HTMLDivElement>(null)
const [isIntersecting, setIntersecting] = useState(false)
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
setIntersecting(entry.isIntersecting)
},
{ threshold: 0.1 }
)
const current = ref.current
if (current) observer.observe(current)
return () => {
if (current) observer.unobserve(current) // ✅ Важно!
}
}, [])
return { ref, isIntersecting }
}
Собираем всё вместе
Финальный компонент будет выглядеть так:
const ImageFeed = () => {
const { ref, isIntersecting } = useScrollTrigger()
const { data, fetchNextPage, hasNextPage, isFetching } = useFetchImages()
useEffect(() => {
if (isIntersecting && hasNextPage && !isFetching) {
fetchNextPage()
}
}, [isIntersecting, hasNextPage, isFetching])
return (
<div>
{data?.pages.map((page) => (
<ImageGrid key={page.page} photos={page.photos} />
))}
<div ref={ref} className="h-2" />
{isFetching && <Loader />}
</div>
)
}
Производительность и оптимизации
- Virtualization: Для больших списков подключаем react-window
- Запросы: throttle на scroll events
- Кеширование: настраиваем через React Query Devtools
- Превью: можно сразу подгружать 2-3 страницы
// Пример расширенной конфигурации
useInfiniteQuery({
// ...,
initialPageParam: 1,
maxPages: 5, // Лимит сохраняемых страниц
refetchOnWindowFocus: false,
})
Заключение
Правильная реализация infinite scroll требует:
- Полного доверия механизмам React Query
- Грамотной работы с Intersection Observer
- Учета особенностей вашего API
Главный урок — не нужно изобретать велосипеды для пагинации. useInfiniteQuery уже содержит все необходимые паттерны, нужно лишь правильно их применить.
Для глубокого погружения рекомендую:
- Документацию TanStack Query v5
- Статью “Infinite Scroll 101” от TkDodo
- Репозиторий react-query-examples
Источник: https://www.reddit.com/r/reactjs/comments/1sw9cmh/infinite_scroll_with_react_query/