Создание бесконечной прокрутки с HTMX
Загрузка контента по мере прокрутки пользователем устраняет необходимость кликов по страницам пагинации и создаёт более плавный опыт просмотра. Но традиционная реализация бесконечной прокрутки требует управления intersection observers, отслеживания состояния и написания значительного объёма JavaScript-кода. HTMX предлагает более простой подход: бесконечную прокрутку, управляемую сервером, с использованием HTML-атрибутов.
Это руководство охватывает каноничный паттерн бесконечной прокрутки HTMX, объясняет, когда использовать revealed вместо intersect, и показывает, как создать прогрессивно улучшенную реализацию, которая работает без JavaScript.
Ключевые выводы
- Бесконечная прокрутка HTMX использует паттерн самовоспроизводящегося элемента-загрузчика, где каждый ответ сервера включает следующий загрузчик
- Используйте
revealedдля полностраничной прокрутки иintersect onceдля прокручиваемых контейнеров с overflow - Стройте на основе стандартных ссылок пагинации для прогрессивного улучшения, работающего без JavaScript
- Сервер контролирует всё состояние пагинации через элемент-загрузчик в каждом ответе
Как работает бесконечная прокрутка, управляемая сервером
Основной паттерн размещает HTMX-атрибуты на элементе-”загрузчике” — обычно это последний элемент в вашем списке. Когда этот элемент становится видимым, HTMX запрашивает следующую страницу с вашего сервера. Сервер возвращает HTML, содержащий как новые элементы, так и новый элемент-загрузчик, указывающий на последующую страницу.
Вот базовая структура:
<div id="items">
<div class="item">First item</div>
<div class="item">Second item</div>
<!-- Last rendered item doubles as the loader -->
<div class="item"
hx-get="/items?page=2"
hx-trigger="revealed"
hx-swap="afterend">
Last item of page 1
</div>
</div>
Ответ сервера для страницы 2 включает элементы и новый загрузчик:
<div class="item">First item of page 2</div>
<div class="item">Second item of page 2</div>
<div class="item"
hx-get="/items?page=3"
hx-trigger="revealed"
hx-swap="afterend">
Last item of page 2
</div>
Этот самовоспроизводящийся паттерн продолжается до тех пор, пока сервер не вернёт контент без загрузчика, сигнализируя об окончании доступных данных.
HTMX Intersect vs Revealed: выбор правильного триггера
HTMX предоставляет два триггера, основанных на видимости, и выбор неправильного вызывает распространённые ошибки.
Используйте revealed для полностраничной прокрутки, где прокручивается сам документ. Этот триггер срабатывает, когда элемент входит в область видимости браузера (viewport).
Используйте intersect once, когда контент находится внутри прокручиваемого контейнера с CSS вроде overflow-y: scroll. Триггер revealed отслеживает viewport документа, а не отдельные прокручиваемые элементы, поэтому он не будет корректно срабатывать в overflow-контейнерах. Триггер intersect полагается на современный Intersection Observer API, который поддерживается во всех основных браузерах.
<!-- Full-page scroll: use revealed -->
<div hx-trigger="revealed" hx-get="/more">...</div>
<!-- Scrollable container: use intersect once -->
<div class="scroll-container" style="overflow-y: scroll; height: 400px;">
<div hx-trigger="intersect once" hx-get="/more">...</div>
</div>
Модификатор once предотвращает дублирующие запросы, если элемент многократно входит и выходит из порога пересечения.
Стратегии замены: afterend vs beforeend
Ваша стратегия замены определяет, где появляется новый контент и как предотвратить дублирующие загрузки.
afterend вставляет контент сразу после элемента-загрузчика. Используйте это, когда загрузчик является последним отрендеренным элементом — ответ появляется под ним.
beforeend добавляет контент внутрь целевого контейнера. Комбинируйте это с hx-target для указания, куда должны попасть элементы:
<div hx-get="/items?page=2"
hx-trigger="revealed"
hx-target="#items"
hx-swap="beforeend">
Loading indicator...
</div>
С beforeend ваш загрузчик обычно находится вне контейнера элементов и заменяется или удаляется после загрузки финальной страницы.
Discover how at OpenReplay.com.
Прогрессивное улучшение со стандартной пагинацией
Бесконечная прокрутка с HTMX должна строиться поверх существующей пагинации, а не полностью её заменять. Начните с работающих ссылок пагинации:
<div id="items">
{% for item in items %}
<div class="item">{{ item.name }}</div>
{% endfor %}
</div>
<a href="/items?page={{ next_page }}">Load more</a>
Затем улучшите ссылку пагинации HTMX-атрибутами:
<a href="/items?page={{ next_page }}"
hx-get="/items?page={{ next_page }}"
hx-trigger="revealed"
hx-target="#items"
hx-swap="beforeend"
hx-select="#items > *">
Load more
</a>
Пользователи без JavaScript видят стандартную пагинацию. Пользователи с HTMX получают бесконечную прокрутку. Серверная конечная точка остаётся идентичной для обоих случаев.
Обновление элементов UI с помощью out-of-band замен
Иногда нужно обновить элементы вне основной области контента — например, счётчики элементов или состояния загрузки. HTMX out-of-band замены справляются с этим:
<!-- In server response -->
<div class="item">New item</div>
<span id="item-count" hx-swap-oob="outerHTML">Showing 20 of 100</span>
Атрибут hx-swap-oob="outerHTML" указывает HTMX найти и заменить элемент с соответствующим ID в любом месте страницы, независимо от основной цели замены.
Стратегии серверной пагинации
Ваш сервер полностью контролирует состояние пагинации. Два распространённых подхода:
Пагинация по смещению (offset) использует номера страниц или значения смещения: /items?page=3 или /items?offset=20. Проста в реализации, но может показывать дубликаты, если элементы добавляются во время просмотра.
Курсорная пагинация использует указатель на последний увиденный элемент: /items?after=item_xyz. Более надёжна для динамического контента, но требует стабильных идентификаторов.
Любой подход работает с HTMX — клиент просто передаёт любой параметр, который сервер предоставляет в следующем элементе-загрузчике.
Заключение
Бесконечная прокрутка HTMX переносит сложность на сервер, где логика пагинации уже существует. Выбирайте revealed для прокрутки документа и intersect once для overflow-контейнеров. Стройте на основе стандартной пагинации, чтобы ваша функциональность корректно деградировала. Позвольте серверу управлять состоянием через элемент-загрузчик каждого ответа, вместо отслеживания страниц на стороне клиента.
Часто задаваемые вопросы
Обычно это происходит при использовании intersect без модификатора once. Элемент может многократно пересекать порог пересечения по мере загрузки контента и изменения макета. Добавьте once к вашему триггеру, например hx-trigger intersect once, чтобы гарантировать только один запрос на элемент-загрузчик.
Добавьте атрибут hx-indicator, указывающий на ваш элемент загрузки. Например, hx-indicator со значением hash loading на вашем элементе-загрузчике и отдельный span с id loading, содержащий ваш спиннер. HTMX автоматически показывает этот элемент во время запросов и скрывает его по завершении.
HTMX помогает предотвратить перекрывающиеся запросы, и каждый элемент-загрузчик обычно настроен на срабатывание один раз. Ответ сервера определяет следующую страницу. Если вам нужна более строгая координация, вы можете использовать hx-sync для управления поведением запросов.
Да. При изменении фильтров сбросьте ваш контейнер контента и обновите URL загрузчика, включив параметры фильтрации. Сервер обрабатывает логику фильтрации и возвращает соответствующую первую страницу. Каждый последующий загрузчик включает те же параметры фильтрации для поддержания согласованности между страницами.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.