Бесконечная прокрутка (infinite scroll) - это технологический приём, который подгружает новый контент на страницу когда пользователь прокручивает страницу вниз.
Попробуем реализовать её при использовании библиотеки React и Intersection Observer API.
Подготовка
Создадим проект с такой структурой:
Посты будем загружать через API JSONPlaceholder. При get-запросе по адресу jsonplaceholder.typicode.com/posts с параметрами limit и page можно получить порцию объектов-постов (не больше 100 штук):
[
{ id: 1, title: '...' },
{ id: 2, title: '...' },
{ id: 3, title: '...' },
/* ... */
{ id: 100, title: '...' },
];
PostList.jsx:
import React, { useState, useEffect } from "react";
import axios from "axios";
import PostItem from "./PostItem";
import "./PostList.scss";
const PostList = () => {
const [posts, setPosts] = useState({ data: [], page: 1 });
const portion = 20;
const totalPages = Math.ceil(100 / portion);
const getNewPosts = () => {
axios
.get("https://jsonplaceholder.typicode.com/posts", {
params: {
_limit: portion,
_page: posts.page,
},
})
.then(({ data }) => {
setPosts({
data: [...posts.data, ...data],
page: posts.page + 1
});
});
};
//загрузка самой первой порции данных
useEffect(() => {
getNewPosts();
}, []);
return (
<div className="post-list">
{posts.data.map((item) => {
return <PostItem key={item.id} info={item} />;
})}
</div>
);
};
export default PostList;
PostItem.jsx:
import React from "react";
const PostItem = (props) => {
const content = `${props.info.id} ${props.info.title}`;
return <div className="post-list__item" ref={ref}>{content}</div>;
};
export default PostItem;
Визуально у меня это выглядит примерно так:
Получение ссылки на последний загруженный элемент
Для реализации бесконечной прокрутки удобнее всего привязываться к последнему загруженному элементу - и, когда он попадет в зону видимости, подгружать следующую порцию данных.
Для того, чтобы можно было получить ссылку на дочерний компонент, нужно обернуть его в React.forwardRef
:
import React, { forwardRef } from "react";
//Оборачиваем компонент элемента списка в React.forwardRef
const PostItem = forwardRef((props, ref) => {
const content = `${props.info.id} ${props.info.title}`;
return <div className="post-list__item" ref={ref}>{content}</div>;
});
export default PostItem;
Затем в родительском элементе запомним эту ссылку:
//создаём переменую для хранения ссылок
const lastItem = createRef();
return (
<div className="post-list">
{posts.data.map((item, index) => {
//если компонент последний в списке,
if (index + 1 === posts.data.length) {
//сохраняем на него ссылку через передачу ему пропс ref
return <PostItem key={item.id} info={item} ref={lastItem} />;
}
return <PostItem key={item.id} info={item} />;
})}
</div>
);
Обратите внимание, что ref мы пишем только в последнем компоненте массива.
Опишем функцию, которая будет вызываться при попадании последнего элемента в область видимости
Intersection Observer API позволяет указать функцию, которая будет вызвана всякий раз при попадании элемента в область видимости пользователем (на экран). Опишем её:
const actionInSight = (entries) => {
if (entries[0].isIntersecting && posts.page <= totalPages) {
getNewPosts();
}
};
actionInSight - callback-функция, вызываемая при попадании объекта в область видимости. В неё передается массив объектов наблюдения. Объект у нас один, так что сразу обращаемся к первому (нулевому) элементу.
Элемент массива entries
описывает пересечение между целевым элементом и его корневым контейнером в определённый момент перехода. В нём содержатся различные данные об этом событии, например, время пересечия или доля попадания объекта в область видимости. Нас интересует только свойство isIntersecting
, оно принимает значение true
, когда интересующий нас элемент попадает на экран.
Условие && posts.page <= totalPages
не даст нам делать запросы, когда количество постов достигнет максимума. Это условие можно заменить на то, которое будет подходить к вашей ситуации.
Зарегистрировать наблюдателя на последний загруженный элемент
Для того, чтобы этот объект сохранялся независимо от рендера компонента, воспользуемся свойством хука useRef.
Установку объекта-наблюдателя оборачиваем в useEffect
, чтобы при изменении последнего элемента вешать на него новый объект-наблюдатель (и отключить старые).
//константа для хранения идентификатора наблюдателя
const observerLoader = useRef();
//действия при изменении последнего элемента списка
useEffect(() => {
//удаляем старый объект наблюдателя
if (observerLoader.current) {
observerLoader.current.disconnect();
}
//создаём новый объект наблюдателя
observerLoader.current = new IntersectionObserver(actionInSight);
//вешаем наблюдателя на новый последний элемент
if (lastItem.current) {
observerLoader.current.observe(lastItem.current);
}
}, [lastItem]);
Заключение
Всё, этого достаточно для создания списка с бесконечной подгрузкой. Полный код примера можно рассмотреть в демке codesandbox
Top comments (0)