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

Сейчас изучаю как JS так и React Сделал сетку контента которая идет из условно бесконечного массива.

Теперь хочу прикрутить к ней бесконечную прокрутку. Нашел варианты с подгрузкой контента (из страниц) Готовые модули тоже нашел, но т.к. это вопрос изучения, хочу реализовать сам.

Сейчас, задачи вижу такие. При didMount, загружать условные 10 блоков При достижения скролом определенного положения, делать setstate для добавления еще 10ти блоков и как-то сохранять положение скрола.

Как получить информацию, что проскролилось что-то и на нужную высоту? Можете подсказать куда копать? Или хорошие примері, в которіх может разобраться новичок.


Ответы (1 шт):

Автор решения: andrewkiri

Вам нужно сделать следующие вещи:

  • Создать компонент который будет оборачивать компоненты созданные на основе вашего условно бесконечно массива. (дальше будем называть его "ленивый" загрузчик)
  • Добавить скрытый элемент после "детей" который будет служить якорем-индикатором конца списка
  • Обеспечить компонентом поддержку отслеживания высоты вертикального скролла посредством props
  • Поместить "ленивый" загрузчик внутрь компонента который имеет вертикальный скролл (как ребенка)
  • Отслеживать вертикальный скролл в скролл контейнере при этом обновляя состояние на уровень выше, также передавая состояние внутрь "ленивому" загрузчику
  • Задать локальное состояние в "ленивом" загрузчике при каком значении он должен вызывать догружающую список функцию (обычно это нижний край компонента)
  • [Опционально] добавить поддержку граничного отступа при котором "догрузка" элементов списка должна случится
  • [Опционально] можно добавить состояние-флаг isLoading которое будет служить барьером дополнительной догрузки если эвент вызывается повторно (что бы обезопаситься от лишней догрузки элементов списка)
  • Отслеживать референции к сколл контейнеру и нижнему якорю с помощью реакт референций документация
  • Отслеживать позиции с помощью .getBoundingClientRect() (документация)

Ссылка на работающий пример на codesandbox

Примечания по примеру:

  • Использовано useMount из библиотеки react-use для вызова функции при посадке компонента в DOM структуру документа
  • Использовано throttle из библиотеки lodash для уменьшения количества вызовов функции догрузчика
  • Использовано библиотеку nanoid для быстрого создания списка с уникальными полями
  • Использовано библиотеку styled-components для улучшения демонстрации
  • Допустимый отступ rootBottomMargin не передается так как имеется значение по умолчанию, которое равно 0 пикселей (при пересечении границы якоря)
  • Скролл контейнер имеет зеленый ободок
  • "Ленивый" загрузчик имеет синий ободок
  • Нижний якорь имеет желто-зеленый ободок

Преимущества подхода

  • Список и его элементы не нуждаются в никаком изменении, не должны ни за чем следить, не нуждаются в дополнительных функциях-обработчиках и не исполняют дорогостоящих операций при скролл евенте (все это вещи о которых сам список элементов не должен беспокоится)
  • Нету лишних функций реагирующих на очень частый скролл-эвент, что в последствии может негативно сказаться на производительности страницы
  • Можно добавить "ленивую" догрузку в любое место приложения, достаточно только завернуть список в ScrollContainer вместе с LazyLoader, передать данные и подключить обработчики
  • Применим к компонентам-функциям и к компонентам-классам одинаково, достаточно лишь обернуть нужные компоненты и создать состояние в родителе
  • Легко расширить до поддержки показа "скелетона", искусственного списка элементов который будет показываться пока идет "догрузка" новых элементов массива (например из API)

Среди недостатков:

  • Использует достаточно много переменных состояния

App.js:

    import React, { useState, useCallback } from "react";
    import throttle from "lodash/throttle";
    import nanoid from "nanoid";
    import "./styles.css";

    import List from "./List";
    import LazyLoader from "./LazyLoader";
    import ScrollContainer from "./ScrollContainer";

    export default function App() {
      const [scrollContainerRect, setScrollContainerRect] = useState({});
      const [scrollTop, setScrollTop] = useState(0);
      const [items, setItems] = useState(
        Array(50)
          .fill("")
          .map(nanoid)
      );

      // Обработчик скролл эвента
      const onScroll = useCallback(
        throttle(({ scrollTop, scrollContainerRect }) => {
          setScrollContainerRect(scrollContainerRect);
          setScrollTop(scrollTop);
        }, 1200),
        [setScrollContainerRect, setScrollTop]
      );

      // Получаем координаты скролл-контейнера при посадке на дерево DOM
      const onMount = useCallback(
        ref => {
          setScrollContainerRect(ref.getBoundingClientRect());
        },
        [setScrollContainerRect]
      );

      // Функция доклеивающая доп элементы массива
      const appendItems = useCallback(() => {
        setItems([
          ...items,
          ...Array(10)
            .fill("")
            .map(nanoid)
        ]);
      }, [items, setItems]);

      return (
        <div className="App">
          <ScrollContainer onScroll={onScroll} onMount={onMount}>
            <LazyLoader
              scrollTop={scrollTop}
              scrollContainerRect={scrollContainerRect}
              onIntersection={appendItems}
            >
              <List items={items} />
            </LazyLoader>
          </ScrollContainer>
        </div>
      );
    }

LazyLoader.js

    import React, { useState, useEffect, useRef } from "react";
    import styled from "styled-components";

    const Container = styled.div`
      border: 4px solid blue;
    `;
    const Bottom = styled.div`
      border: 2px solid yellowgreen;
    `;

    const LazyLoader = ({
      scrollTop,
      rootBottomMargin = 0,
      scrollContainerRect,
      onIntersection,
      children
    }) => {
      const [scrollThreshold, setScrollThreshold] = useState(0);
      // Референция к искусственному элементу который рендериться после списка
      const fakeBottomRef = useRef(null);

      // получаем координаты элемента
      const fakeBottomRect = fakeBottomRef.current
        ? fakeBottomRef.current.getBoundingClientRect()
        : {};


      useEffect(() => {
        // Обновляем значение при котором должен вызваться колбек onIntersection
        setScrollThreshold(
          fakeBottomRect.top - scrollContainerRect.top - scrollContainerRect.height
        );
      }, [scrollTop, scrollContainerRect, fakeListRect.top]);

      useEffect(() => {
        // Следим за граничным значением и вызваем onIntersection когда надо
        if (scrollThreshold < rootBottomMargin) {
          onIntersection();
        }
      }, [scrollThreshold, rootBottomMargin, onIntersection]);

      return (
        <Container>
          {children}
          <Bottom ref={fakeBottomRef} />
        </Container>
      );
    };

    export default LazyLoader;

ScrollContainer.js

    import React, { useRef, useCallback } from "react";
    import { useMount } from "react-use";
    import styled from "styled-components";

    const Container = styled.div`
      max-height: 400px;
      overflow-y: scroll;
      border: 4px solid green;
    `;

    const ScrollContainer = ({ children, onScroll, onMount }) => {
      const containerRef = useRef(null);

      useMount(() => {
        onMount(containerRef.current);
      });

      const handleScroll = useCallback(
        event => {
          const { target } = event;
          const scrollContainerRect = target.getBoundingClientRect();
          const scrollTop = target.scrollTop;

          onScroll({ scrollTop, scrollContainerRect });
        },
        [onScroll]
      );

      return (
        <Container onScroll={handleScroll} ref={containerRef}>
          {children}
        </Container>
      );
    };

    export default ScrollContainer;

List.js

    import React, { Fragment } from "react";
    import styled from "styled-components";

    const Container = styled.div``;
    const Item = styled.div`
      margin: 10px 0;
    `;

    const List = ({ items }) => (
      <Container>
        {items.map(id => (
          <Fragment key={id}>
            <Item>I'm item with id {id}</Item>
          </Fragment>
        ))}
      </Container>
    );

    export default List;

UPDATE

Версия с очень простым реакт хуком

Преимущества подхода:

  • Очень компактный вариант исполнения функционала
  • Можно поместить в любой компонент, при условии что это компонент-функция
  • Использует минимум переменных состояния

Среди недостатков:

  • Нельзя применить в компонентах-классах

Код на codesandbox

// App.js
export default function App() {
  const [items, setItems] = useState(
    Array(50)
      .fill("")
      .map(nanoid)
  );

  // функция "доклеивающая" элементы массива
  const appendItems = useCallback(() => {
    setItems([
      ...items,
      ...Array(10)
        .fill("")
        .map(nanoid)
    ]);
  }, [items, setItems]);

  // custom-hook отслеживающий скролл посредством референции и функции 
  // обработчика скролл эвента
  const [onScroll, containerRef] = useLazyLoading({
    onIntersection: appendItems,
    delay: 1200
  });

  return (
    <div className="App">
      <Container ref={containerRef} onScroll={onScroll}>
        <List items={items} />
      </Container>
    </div>
  );
}

// useLazyLoading.js
import { useRef, useCallback } from "react";
import throttle from "lodash/throttle";

export function useLazyLoading({
  onIntersection,
  delay = 1000,
  marginFromBottom = 10
}) {
  const containerRef = useRef(null);

  // Функция обработчик scroll эвента, с ограничением количества вызовов
  // посредством lodash/throttle
  const onScroll = useCallback(
    throttle(() => {
      const containerScrollTop = containerRef.current.scrollTop;
      const containerHeight = containerRef.current.clientHeight;
      const scrollHeight = containerRef.current.scrollHeight;
      if (
        scrollHeight -
          containerScrollTop -
          containerHeight -
          marginFromBottom <=
        0
      ) {
        onIntersection();
      }
    }, delay),
    [onIntersection, containerRef, marginFromBottom, delay]
  );

  return [onScroll, containerRef];
}
→ Ссылка