Типизация и детерминированная обработка состояния скролла при виртуализации списков с I...
Типизация и детерминированная обработка состояния скролла при виртуализации списков с IntersectionObserver и кастомным Suspense в React 19 IntersectionObserver на бесконечных списках часто порождает гонки: элемент пересек границу, запрос ушел, а пользователь улетел скроллом дальше. Результат — повторные вызовы API и пустые мертвые зоны в рендере. В React 19 это решается через Suspense и хук use, делая обработку скролла детерминированной. Замкнутое состояние скролла Вместо разрозненных useState для startIndex и endIndex используем явный интерфейс ScrollState. Он обновляется атомарно — нельзя изменить один индекс без другого. Это исключает рассинхрон и гарантирует, что стейт всегда консистентен при передаче в Suspense-зависимый компонент. interface ScrollState { startIndex: number; endIndex: number; } Синхронизация через Suspense и use Когда IntersectionObserver срабатывает, мы сетим новый ScrollState. Компонент ItemsLoader внутри Suspense использует use(fetchItems(scrollState)) — это приостанавливает рендер до разрешения промиса. Наблюдатель пересоздается только в useEffect, который зависит от state.endIndex, то есть после того, как предыдущий диапазон гарантированно зафиксирован в DOM. Типичная ошибка и trade-off Ошибка: пытаться блокировать скролл флагами isFetching или отключать observer вручную. Это ломает UX и не решает проблему гонок, так как флаг может быть false до завершения ререндера. Минус подхода — он ломает классический поток «эффект -> стейт -> ререндер», требуя отказа от кастомных useEffect-решений. Но плюс — полная детерминированность: новые данные грузятся только после рендера старых. Практический совет Для production убедись, что fetchItems внутри use возвращает не просто массив, а структуру с totalCount или hasMore — это позволяет корректно остановить наблюдение при достижении конца списка. Иначе observer будет срабатывать на последнем sentinel-элементе бесконечно. Вывод: Замыкание скролл-состояния в атомарный interface и ожидание рендера через Suspense устраняет гонки IntersectionObserver без костылей, делая поведение списка полностью предсказуемым для пользователя и инженера.