Infinite scroll

Infinite Scroll — это техника динамической подгрузки данных при прокрутке, которая позволяет отображать большое количество элементов без необходимости загружать их все сразу. В библиотеке Naive UI эта функциональность реализуется через компонент NList с сочетанием scroll events и реактивного состояния Vue.

Использование NList и обработка событий прокрутки

Компонент NList в Naive UI поддерживает стандартные события скролла. Для реализации бесконечной прокрутки создаются реактивные массивы данных и функции, которые подгружают новые элементы при достижении конца списка.

Пример базовой структуры:

<template>
  <n-list
    :data="items"
    :virtual-scroll="true"
    @scroll="handleScroll"
  >
    <template #default="{ item }">
      <n-list-item>{{ item.name }}</n-list-item>
    </template>
  </n-list>
</template>

<script setup>
import { ref } from 'vue'

const items = ref(Array.from({ length: 20 }, (_, i) => ({ name: `Элемент ${i + 1}` })))

function handleScroll(event) {
  const scrollContainer = event.target
  if (scrollContainer.scrollTop + scrollContainer.clientHeight >= scrollContainer.scrollHeight) {
    loadMore()
  }
}

function loadMore() {
  const nextItems = Array.from({ length: 10 }, (_, i) => ({ name: `Элемент ${items.value.length + i + 1}` }))
  items.value.push(...nextItems)
}
</script>
  • virtual-scroll обеспечивает рендер только видимых элементов, что значительно увеличивает производительность при работе с большими списками.
  • Событие scroll позволяет отслеживать положение скролла и инициировать подгрузку новых данных при достижении нижней границы контейнера.

Оптимизация производительности

  1. Использование виртуального скролла Для списков с сотнями или тысячами элементов обязательно включать virtual-scroll, иначе рендер всех элементов приведёт к торможению интерфейса.

  2. Дебаунс события прокрутки Частые события скролла могут вызывать множественные вызовы функции подгрузки. Оптимально использовать debounce:

import { debounce } from 'lodash'

const handleScroll = debounce((event) => {
  const scrollContainer = event.target
  if (scrollContainer.scrollTop + scrollContainer.clientHeight >= scrollContainer.scrollHeight) {
    loadMore()
  }
}, 200)
  1. Флаг загрузки Для предотвращения многократного вызова loadMore при медленном отклике сервера используют флаг loading:
const loading = ref(false)

async function loadMore() {
  if (loading.value) return
  loading.value = true
  const nextItems = await fetchNextItems()
  items.value.push(...nextItems)
  loading.value = false
}

Подгрузка данных с сервера

Infinite Scroll часто используется с API. В Naive UI комбинация реактивных массивов и async/await позволяет элегантно подгружать данные:

async function fetchNextItems() {
  const response = await fetch(`/api/items?offset=${items.value.length}&limit=20`)
  const data = await response.json()
  return data.map(item => ({ name: item.title }))
}
  • Подгружаемые элементы можно сразу форматировать под компоненты NListItem.
  • Следует учитывать, что сервер должен поддерживать пагинацию (offset и limit), иначе список может бесконтрольно расти.

Индикатор загрузки и пустой список

Для улучшения UX можно использовать компоненты NSpin и NEmpty:

<n-list :data="items" @scroll="handleScroll">
  <template #default="{ item }">
    <n-list-item>{{ item.name }}</n-list-item>
  </template>
  <template #footer>
    <n-spin v-if="loading" size="small" />
    <n-empty v-else-if="items.length === 0" description="Список пуст" />
  </template>
</n-list>
  • #footer позволяет отобразить индикатор загрузки прямо внизу списка.
  • NEmpty сообщает пользователю о том, что данных пока нет.

Обработка ошибок и конец данных

Важно учитывать сценарии, когда сервер возвращает пустой массив или происходит ошибка. Для этого вводят флаг hasMore:

const hasMore = ref(true)

async function loadMore() {
  if (!hasMore.value || loading.value) return
  loading.value = true
  try {
    const nextItems = await fetchNextItems()
    if (nextItems.length === 0) hasMore.value = false
    items.value.push(...nextItems)
  } catch (e) {
    console.error('Ошибка при подгрузке:', e)
  } finally {
    loading.value = false
  }
}
  • Флаг hasMore предотвращает лишние запросы, когда данные закончились.
  • Ошибки логируются и могут отображаться через уведомления (NMessage, NNotification).

Комбинация с фильтрацией и сортировкой

При использовании Infinite Scroll вместе с фильтрами или сортировкой следует:

  1. Сбрасывать массив items при изменении фильтра.
  2. Сбрасывать offset или номер страницы.
  3. Включать индикатор загрузки для каждого обновления.
watch(filter, async () => {
  items.value = []
  offset.value = 0
  hasMore.value = true
  await loadMore()
})

Это гарантирует корректное отображение данных при изменении условий запроса.

Поддержка мобильных устройств

На мобильных важно учитывать:

  • Размер touch-target элементов (NListItem)
  • Отложенную загрузку через IntersectionObserver вместо scroll для более плавного UX и меньшей нагрузки на рендер.
const observer = new IntersectionObserver(([entry]) => {
  if (entry.isIntersecting) loadMore()
}, { threshold: 1.0 })

onMounted(() => {
  observer.observe(document.querySelector('#list-end-marker'))
})
  • Элемент #list-end-marker размещается внизу списка и служит триггером подгрузки новых элементов.

Вывод

Infinite Scroll в Naive UI реализуется через комбинацию реактивного состояния, компонентов NList, виртуального скролла и отслеживания позиции скролла. Для производительного и удобного интерфейса рекомендуется:

  • включать virtual-scroll для больших списков;
  • использовать debounce и флаги загрузки;
  • предусматривать индикаторы загрузки и обработку конца данных;
  • интегрировать пагинацию сервера и фильтры;
  • применять IntersectionObserver на мобильных для плавной подгрузки.

Такая архитектура обеспечивает плавную и масштабируемую реализацию бесконечной прокрутки для любых приложений на Vue 3 с использованием Naive UI.