Бесконечная прокрутка на React
Tags: react, tutorial, javascript, intersectionobserver
Published: 2023-03-28 13:12:13

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

![Pagination and infinite scroll](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yscxc4aduarljr8qxih3.jpg)
Попробуем реализовать её при использовании библиотеки React и Intersection Observer API.

## Подготовка
Создадим проект с такой структурой:
![Компонент PostList с вложенными компонентами PostItem](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ezgpkk3o5osqo0kypvad.png)
Посты будем загружать через [API JSONPlaceholder](http://jsonplaceholder.typicode.com/). При get-запросе по адресу [jsonplaceholder.typicode.com/posts](https://jsonplaceholder.typicode.com/posts) с параметрами limit и page можно получить порцию объектов-постов (не больше 100 штук):

```json
[
  { id: 1, title: '...' },
  { id: 2, title: '...' },
  { id: 3, title: '...' },
  /* ... */
  { id: 100, title: '...' },
];
```
PostList.jsx:

```javascript
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:

```javascript
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;
```
Визуально у меня это выглядит примерно так:
![Последовательность блоков с текстом](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tp24asgkllqqxuvg5z8h.png)

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

![Схема того, как последний загруженный элемент пересекает границу видимой области экрана](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b5evhsq589mrkzu0coy8.png)

Для того, чтобы можно было получить ссылку на дочерний компонент, нужно обернуть его в `React.forwardRef` :

```javascript
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;
```
Затем в родительском элементе запомним эту ссылку:

```javascript
//создаём переменую для хранения ссылок
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 мы пишем только в **последнем компоненте** массива.

![Пропс ref передаётся только последнему компоненту массива](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zllb53rx21zv52hmo2hn.png)

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

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

```javascript
const actionInSight = (entries) => {
  if (entries[0].isIntersecting && posts.page <= totalPages) {
    getNewPosts();
  }
};
```

**actionInSight** - callback-функция, вызываемая при попадании объекта в область видимости. В неё передается массив объектов наблюдения. Объект у нас один, так что сразу обращаемся к первому (нулевому) элементу.

Элемент массива `entries` описывает пересечение между целевым элементом и его корневым контейнером в определённый момент перехода. В нём содержатся различные данные об этом событии, например, время пересечия или доля попадания объекта в область видимости. Нас интересует только свойство `isIntersecting`, оно принимает значение `true`, когда интересующий нас элемент попадает на экран.

Условие `&& posts.page <= totalPages` не даст нам делать запросы, когда количество постов достигнет максимума. Это условие можно заменить на то, которое будет подходить к вашей ситуации.

## Зарегистрировать наблюдателя на последний загруженный элемент

Для того, чтобы этот объект сохранялся независимо от рендера компонента, воспользуемся [свойством хука useRef](https://bookflow.ru/umnyj-sposob-ispolzovaniya-huka-useref-v-react/).

Установку объекта-наблюдателя оборачиваем в `useEffect`, чтобы при изменении последнего элемента вешать на него новый объект-наблюдатель (и отключить старые).

```javascript
//константа для хранения идентификатора наблюдателя
const observerLoader = useRef();

//действия при изменении последнего элемента списка
useEffect(() => {
  //удаляем старый объект наблюдателя
  if (observerLoader.current) {
    observerLoader.current.disconnect();
  }

  //создаём новый объект наблюдателя
  observerLoader.current = new IntersectionObserver(actionInSight);

  //вешаем наблюдателя на новый последний элемент
  if (lastItem.current) {
    observerLoader.current.observe(lastItem.current);
  }
}, [lastItem]);
```

![Изменение последнего загруженного элемента](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n152bsmig128z68fhior.png)
## Заключение
Всё, этого достаточно для создания списка с бесконечной подгрузкой. Полный код примера можно рассмотреть в [демке codesandbox](https://codesandbox.io/p/github/lira-bazh/infinite-scroll-in-react/master?workspaceId=2af9775b-a6cf-487d-bfc0-31067b7e0e2b&file=%2Fclient%2Fcomponents%2FPostList.jsx)