DEV Community

Lira
Lira

Posted on

2

Бесконечная прокрутка на 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

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (0)

Heroku

This site is powered by Heroku

Heroku was created by developers, for developers. Get started today and find out why Heroku has been the platform of choice for brands like DEV for over a decade.

Sign Up