Как реализовать загрузку следующей страницы данных с сохранением состояния и обработкой ошибок
Что проверяют
Умение довести пагинацию до продового качества: догрузка без дублей, устойчивость к гонкам, поведение при ошибке и сохранение уже загруженного. Код на десять строк со слушателем scroll считается заготовкой, а не ответом.
Как отвечать
Состояние: список элементов, курсор или номер страницы, hasMore, loading, error. Принципиальный момент: ошибка и загрузка относятся к попытке, а не ко всему списку. При неудачном запросе уже показанные элементы остаются на экране, а внизу появляется блок с кнопкой повтора.
Защита от повторных запросов делается через ref, а не через state: обработчик события замыкает старое значение, и проверка if (loading) return по состоянию пропускает второй вызов.
const inFlight = useRef(false);
const loadNext = useCallback(async () => {
if (inFlight.current || !hasMore) return;
inFlight.current = true;
setError(null);
try {
const res = await fetch(`/api/items?cursor=${cursor ?? ""}`, { signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const { items, nextCursor } = await res.json();
setItems((prev) => [...prev, ...items]);
setCursor(nextCursor);
setHasMore(Boolean(nextCursor));
} catch (e) {
if (e.name !== "AbortError") setError(e.message);
} finally {
inFlight.current = false;
}
}, [cursor, hasMore]);Дальше по пунктам:
- Триггер догрузки:
IntersectionObserverна пустом элементе-сентинеле в конце списка. Он дешевле слушателяscroll, не дёргает layout на каждый пиксель и корректно работает во вложенном скролле. - Гонки:
AbortControllerна предыдущий запрос либо проверка, что пришедший ответ относится к актуальному курсору. Иначе медленный ответ третьей страницы приедет после четвёртой и перемешает порядок. - Курсор против offset:
offset/limitдаёт дубли и пропуски, если между запросами что-то вставили или удалили. Для лент берут курсорную пагинацию по устойчивому ключу (idилиcreated_atплюсid). - Готовое решение:
useInfiniteQueryиз TanStack Query закрывает кеш страниц, дедупликацию, ретраи и восстановление данных при возврате назад. СвойsessionStorageнужен, только если библиотеки в проекте нет. - UX: скелетоны вместо скачков высоты, восстановление позиции скролла при возврате, кнопка «Загрузить ещё» как альтернатива бесконечному скроллу (иначе футер недостижим, а с клавиатуры лента непроходима).
- Виртуализация списка, когда элементов тысячи и DOM начинает тормозить.
Где спотыкаются
Вешают обработчик scroll без троттлинга и без блокировки повторного вызова, получают несколько параллельных запросов одной страницы и дубли. Второй промах: при ошибке очищают весь массив, и пользователь теряет десять уже пролистанных экранов.