DEV Community

Lira
Lira

Posted on

Бесконечная прокрутка на React

Бесконечная прокрутка (infinite scroll) - это технологический приём, который подгружает новый контент на страницу когда пользователь прокручивает страницу вниз.

Pagination and infinite scroll
Попробуем реализовать её при использовании библиотеки React и Intersection Observer API.

Подготовка

Создадим проект с такой структурой:
Компонент PostList с вложенными компонентами PostItem
Посты будем загружать через API JSONPlaceholder. При get-запросе по адресу jsonplaceholder.typicode.com/posts с параметрами limit и page можно получить порцию объектов-постов (не больше 100 штук):

[
  { id: 1, title: '...' },
  { id: 2, title: '...' },
  { id: 3, title: '...' },
  /* ... */
  { id: 100, title: '...' },
];
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Визуально у меня это выглядит примерно так:
Последовательность блоков с текстом

Получение ссылки на последний загруженный элемент

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

Схема того, как последний загруженный элемент пересекает границу видимой области экрана

Для того, чтобы можно было получить ссылку на дочерний компонент, нужно обернуть его в 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;
Enter fullscreen mode Exit fullscreen mode

Затем в родительском элементе запомним эту ссылку:

//создаём переменую для хранения ссылок
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>
);
Enter fullscreen mode Exit fullscreen mode

Обратите внимание, что ref мы пишем только в последнем компоненте массива.

Пропс ref передаётся только последнему компоненту массива

Опишем функцию, которая будет вызываться при попадании последнего элемента в область видимости

Intersection Observer API позволяет указать функцию, которая будет вызвана всякий раз при попадании элемента в область видимости пользователем (на экран). Опишем её:

const actionInSight = (entries) => {
  if (entries[0].isIntersecting && posts.page <= totalPages) {
    getNewPosts();
  }
};
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

Изменение последнего загруженного элемента

Заключение

Всё, этого достаточно для создания списка с бесконечной подгрузкой. Полный код примера можно рассмотреть в демке codesandbox

Top comments (0)