Бесконечная прокрутка в React: руководство и лучшие практики

Бывали ли у вас сайты или приложения, которые подгружают и отображают новый контент по мере прокрутки страницы? Это и называется бесконечной прокруткой (infinite scroll).
Бесконечная прокрутка — популярная техника для просмотра большого объёма контента. Она может сделать навигацию плавнее и удобнее, особенно на мобильных устройствах, но также влечёт за собой ряд ограничений и рисков (производительность, доступность, SEO, тестирование).
Что такое бесконечная прокрутка и когда её применять
Коротко: бесконечная прокрутка загружает дополнительные элементы по мере прокрутки страницы; обычно это реализуется через обработчики событий прокрутки, готовые компоненты или API браузера (Intersection Observer).
Когда использовать:
- Фиды контента (лентa соцсетей, маркетплейсы с большим числом карточек).
- Сценарии, где пользователь ожидает «непрерывного» просмотра.
Когда не использовать:
- Если пользователю важна точная навигация по страницам (SEO-страницы поиска, каталоги с фильтрами, где нужен сохранный URL и пагинация).
- Если высокий риск, что пользователь пропустит важный контент (контент, требующий обязательного просмотра).
Важно: альтернативы — классическая пагинация, кнопка «Загрузить ещё» или гибрид «пагинация + бесконечная прокрутка».
Основные способы реализации в React
Коротко описанные варианты:
- Библиотеки (react-infinite-scroll-component и аналоги) — быстрый путь с готовыми обработчиками состояния.
- Хуки и нативные события scroll — простая реализация, но требует debounce/throttle и аккуратной работы с состоянием.
- Intersection Observer — современный и предпочтительный способ: экономный по CPU, не зависит от частоты событий scroll.
Примеры из практики: react-infinite-scroll-component
Удобная библиотека для быстрого старта. Ниже — пример с класс-компонентом.
npm install react-infinite-scroll-component --saveimport React from 'react'
import InfiniteScroll from 'react-infinite-scroll-component'
class App extends React.Component {
constructor() {
super()
this.state = {
items: [],
hasMore: true
}
}
componentDidMount() {
this.fetchData(1)
}
fetchData = (page) => {
const newItems = []
for (let i = 0; i < 100; i++) {
newItems.push(i )
}
if (page === 100) {
this.setState({ hasMore: false })
}
this.setState({ items: [...this.state.items, ...newItems] })
}
render() {
return (
Infinite Scroll
Loading...}
endMessage={
Yay! You have seen it all
}
>
{this.state.items.map((item, index) => (
{item}
))}
)
}
}
export default AppОбъяснение ключевых пропсов:
- dataLength — длина текущего массива элементов; библиотека использует это для принятия решения, нужно ли запускать next.
- next — функция, загружающая следующую порцию (обычно вызывает API).
- hasMore — флаг наличия следующей страницы.
- loader / endMessage — отображаемые блоки при загрузке и при окончании данных.
Советы при использовании библиотек:
- Всегда добавляйте обработку ошибок и состояние загрузки (isLoading), чтобы избежать повторных вызовов.
- Применяйте throttle/debounce для fetch-функций, если вы смешиваете с нативными scroll-событиями.
- Контролируйте уникальные ключи элементов для корректного рендера.
Реализация с хуками и нативными событиями scroll
Ниже — пример на функциональном компоненте с useState и useEffect, как в исходном материале.
import React, {useState, useEffect} from 'react'
function App() {
const [items, setItems] = useState([])
const [hasMore, setHasMore] = useState(true)
const [page, setPage] = useState(1)
useEffect(() => {
fetchData(page)
}, [page])
const fetchData = (page) => {
const newItems = []
for (let i = 0; i < 100; i++) {
newItems.push(i)
}
if (page === 100) {
setHasMore(false)
}
setItems([...items, ...newItems])
}
const onScroll = () => {
const scrollTop = document.documentElement.scrollTop
const scrollHeight = document.documentElement.scrollHeight
const clientHeight = document.documentElement.clientHeight
if (scrollTop + clientHeight >= scrollHeight) {
setPage(page + 1)
}
}
useEffect(() => {
window.addEventListener('scroll', onScroll)
return () => window.removeEventListener('scroll', onScroll)
}, [items])
return (
{items.map((item, index) => (
{item}
))}
)
}
export default AppКритические замечания к этому подходу:
- Слушатель scroll вызывается очень часто — нужно использовать throttle/debounce.
- Состояние page должно обновляться через функциональный set (setPage(p => p + 1)), чтобы избежать проблем с замыканиями.
- Для серверной пагинации лучше передавать номер страницы или курсор.
Intersection Observer — рекомендуемый нативный способ
Intersection Observer отслеживает видимость «маяка» внизу списка и подгружает данные только тогда, когда маяк попадает в область видимости. Это экономит ресурсы и точнее, чем постоянный опрос позиции прокрутки.
Пример (фрагмент кода):
import React, { useRef, useState, useEffect } from 'react'
function InfiniteList({ fetchPage }) {
const [items, setItems] = useState([])
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const loader = useRef(null)
useEffect(() => {
const observer = new IntersectionObserver(entries => {
if (entries[0].isIntersecting && hasMore) {
setPage(p => p + 1)
}
}, { root: null, rootMargin: '200px', threshold: 0.1 })
if (loader.current) observer.observe(loader.current)
return () => observer.disconnect()
}, [hasMore])
useEffect(() => {
let cancelled = false
async function load() {
const result = await fetchPage(page)
if (cancelled) return
setItems(prev => [...prev, ...result.items])
setHasMore(result.hasMore)
}
load()
return () => { cancelled = true }
}, [page, fetchPage])
return (
{items.map((item, i) => {item})}
)
}
export default InfiniteListПреимущества Intersection Observer:
- Меньше событий, лучше для батареи и CPU.
- Можно задавать rootMargin, чтобы заранее начать загрузку.
- Работает в большинстве современных браузеров; для старых браузеров — полифилл.
Дизайн API для бесконечной прокрутки (сервер + клиент)
Рекомендации по API:
- Предпочитайте курсорную пагинацию (cursor/continuation token) для стабильности при частых изменениях данных.
- Если используете offset-based пагинацию, внимательно следите за изменениями коллекции (удаление/добавление) — оффсеты могут «перепрыгивать» элементы.
- Ответ API должен содержать массив элементов и флаг/токен следующей страницы (hasMore или nextCursor).
- Пагинация должна возвращать предсказуемое количество элементов (pageSize).
Пример формата ответа:
- { items: […], nextCursor: “abc123”, hasMore: true }
Доступность (a11y) и SEO
Доступность:
- Добавьте фокусируемый «маяк» и aria-live-регионы для уведомления пользователей экранных читалок о новой подгрузке контента.
- Обеспечьте клавиатурную навигацию: пользователь должен иметь возможность добраться до конца списка с клавиатуры и активировать загрузку.
- Предусмотрите «паузы» или кнопку «Загрузить ещё» для пользователей, которым нужна контроль над потоком контента.
SEO:
- Серверная страница или отдельные URL для секций обеспечат индексируемость. Бесконечная прокрутка часто мешает SEO — используйте прогрессивное улучшение: базовая пагинация на сервере + client-side infinite scroll.
- Обновляйте URL (history.pushState) при подгрузке важных «страничных» состояний, только если это логично для пользователя.
Производительность и удержание памяти
Хуки и классы могут накапливать тысячи DOM-элементов. Советы:
- Используйте виртуализацию (react-window, react-virtualized) для больших списков.
- Ограничьте количество одновременно отрисованных элементов (windowing).
- Очистка неиспользуемых данных: при прокрутке вверх/вниз можно удалять старые элементы из состояния и подгружать заново при необходимости.
- Используйте ключи и memoization, чтобы минимизировать перерендеры.
Когда бесконечная прокрутка не сработает — типичные ошибки и контрпримеры
Примеры провальных сценариев:
- Каталог товаров, где пользователю нужно сравнивать товары по страницам и делиться конкретной страницей — бесконечная прокрутка усложнит навигацию и SEO.
- Страницы со смешанным контентом (новости+реклама): реклама может «провалиться» в середину пользовательской ленты, что снизит CTR и приведёт к путанице.
- Сильно динамичный контент (частые добавления/удаления): оффсетная пагинация будет нестабильной.
Альтернативы:
- Кнопка «Показать ещё» — лучший выбор для сохранения контроля и простоты тестирования.
- Классическая пагинация — чёткие URL, простота кеширования и SEO.
Критерии приёмки и тестовые сценарии
Критерии приёмки (acceptance):
- При достижении (или приближении к) конца списка вызывается загрузка следующей порции данных.
- Нет дублирования элементов после нескольких последовательных загрузок.
- Обработка ошибок: при неудаче показывается сообщение и доступна попытка повтора.
- Поток данных останавливается при hasMore=false и показывается сообщение об окончании.
- Поддержка клавиатуры и уведомлений для скринридеров о новой подгрузке.
Тестовые сценарии:
- Фронтенд: имитация медленного соединения — элементы загружаются корректно, loader отображается.
- Нагрузочное тестирование: симуляция одновременных пользователей и множественных подрядных загрузок — отсутствие утечек памяти.
- SEO: проверка доступности контента при отключённом JS (если применимо) или корректная серверная пагинация.
Роли и чек-листы перед релизом
Разработчик:
- Реализовать debounce/throttle и обработку ошибок.
- Добавить логирование загрузок и метрики (latency, error rate).
- Имплементировать очистку слушателей и отмену запросов при анмаунте.
QA:
- Проверить edge-cases: пустой ответ, ошибки сети, быстрые пролистывания.
- Проверить на мобильных устройствах и старых браузерах.
Продукт/PM:
- Принять решение: бесконечная прокрутка, кнопка «Загрузить ещё» или пагинация.
- Оценить влияние на бизнес-метрики: удержание, CTR, конверсии.
Инцидентный план: быстрый откат
Если после релиза поведение пагинации вызывает проблемы (замедления, дубли, ошибки):
- Отключить клиентский бесконечный скролл (feature flag) — вернуть кнопку «Загрузить ещё» или пагинацию.
- Откатить изменения на фронтенде, развернуть горячий фикс.
- Проверить логи API и аналитики: метрики ошибок и времени ответа.
- Проанализировать причину (память, запросы, гонки) и исправить в ветке разработки.
Чек-лист для производительности (короткий)
- Использовать виртуализацию при большом количестве элементов.
- Ограничивать глубину истории состояния (не хранить вечный массив в памяти без лимитов).
- Сжимать возможные payloads на беке — возвращать только необходимые поля.
- Установить разумный pageSize (не слишком большой, не слишком маленький).
Минимальная методология внедрения (шаги)
- Решите, нужен ли бесконечный скролл для задачи.
- Выберите подход: библиотека / Intersection Observer / кнопка «Загрузить ещё».
- Спроектируйте API (cursor vs offset).
- Реализуйте прототип и протестируйте на мобильных и десктопных устройствах.
- Добавьте метрики и проверяйте поведение в продакшене с feature flag.
Краткое резюме
- Бесконечная прокрутка повышает удобство просмотра, но требует дополнительной работы по доступности, SEO и производительности.
- Intersection Observer — предпочтительная нативная техника; для больших списков обязательно используйте виртуализацию.
- Рассматривайте альтернативы (кнопка «Загрузить ещё», пагинация) в зависимости от бизнес-требований.
Примечание: перед массовым внедрением выполните A/B-тестирование, чтобы измерить влияние на поведение пользователей.
Часто задаваемые вопросы
Вопрос: Что лучше — бесконечная прокрутка или пагинация?
Ответ: Зависит от сценария. Для лент и feed’ов бесконечная прокрутка чаще удобнее. Для товаров и страниц, где важен URL и индексируемость — пагинация.
Вопрос: Как избежать утечек памяти при бесконечной прокрутке?
Ответ: Используйте виртуализацию, ограничивайте размер кеша в клиенте и очищайте неиспользуемые слушатели/запросы.
Вопрос: Нужно ли обновлять URL при подгрузке следующей порции?
Ответ: Только если новые порции соответствуют логическому состоянию, к которому пользователь может захотеть вернуться или поделиться ссылкой. В противном случае не стоит.
Похожие материалы
Мониторинг процессов Windows с Kiwi
Как открыть Командную строку в Windows
Как открыть Sticky Notes в Windows 11
Исправить: «Жёсткий диск не установлен» в Windows
Как отключить сохранение разговоров в ChatGPT