Scroll infinito não é algo comum de se ver mas pode ser muito útil e prático na hora de exibir uma lista sem ter que usar uma paginação e precisar de uma maior interação do usuário.

Eu já precisei fazer em um projeto há um tempo e utilizei essa lib que funcionou muito bem, porém, recentemente eu decidi reescrever esse projeto e tentar criar o scroll infinito com react hooks e a IntersectionObserver API, sem nenhuma lib externa, e funcionou muito bem. Por isso gostaria de compartilhar o resultado com vocês.

Ele será bem simples, usarei um componente com um state pra indicar o estado do loading na página, outro para armazenar os dados da nossa listagem e a referência de um componente. Ocultarei o CSS por ser irrelevante mas você pode ver o resultado final no repositório.

Criando nosso componente

Primeiro vamos criar nosso componente que exibirá os dados.

import React, { useState, useRef } from "react";

export default function App() {
  const lastRef = useRef(null);
  const [isLoading, setIsLoading] = useState(false);
  const [todos, setTodos] = useState({
    itens: [],
    page: 0,
    totalPages: 1,
  });

  return (
    <div>
      <h2>Lorem ipsum's list</h2>
      {todos.itens.map(({ title }, index) => {
        return (
          <div key={index}>
            <p>{title}</p>
          </div>
        );
      })}
      {!!todos.itens.length && <div ref={lastRef} />}
      {isLoading && <p>Loading...</p>}
    </div>
  );
}

export default App;

Buscando dados da API

Também precisamos de um método que faça a requisição HTTP buscando os dados de alguma API. Eu criei uma função chamada fetchTodos que recebe o número da página que quero buscar como parâmetro. Pra esse exemplo eu mockei o retorno dessa função, não irei compartilhar o código aqui pois é pra um caso muito específico, se quiser você pode ver o código que eu fiz clicando aqui, o mais importante é que você tenha uma API que retorno no mínimo esse formato de dados:

{
  itens: [{...}],
  totalPages: X,
  page: X,
}

Custom Hook useIsElementVisible

Agora iremos criar nosso custom hook que indicará se o elemento está visível na tela, usando a API IntersectionObserver pra isso, que é nativa dos navegadores.

import { useEffect, useState } from 'react';

export default (el) => {
  const [isVisible, setIsVisible] = useState(false);
  const callback = ([entry]) => {
    setIsVisible(entry.isIntersecting);
  };

  useEffect(() => {
    const watch = new IntersectionObserver(callback);
    if (el) {
      watch.observe(el);
      return () => watch.unobserve(el);
    }
  }, [el]);

  return isVisible && !!el;
};

Nosso objetivo é passar a referência de um elemento em baixo da lista e verificar se ele está visível na tela ou não. Se sim, disparamos a função pra buscar mais itens.

Atualizando nosso componente

Tudo pronto, agora vamos criar nosso método pra buscar os dados. Também usaremos o hook useEffect duas vezes; para disparar a função no carregamento da página e quando o elemento a baixo da lista estiver visível.

import React, { useState, useEffect, useRef } from "react";
import useIsElementVisible from "./hooks/useIsElementVisible";
import { fetchTodos } from "./services";

export default function App() {
  const lastRef = useRef(null);
  const [isLoading, setIsLoading] = useState(false);
  const [todos, setTodos] = useState({
    itens: [],
    page: 0,
    totalPages: 1,
  });

  const isLastVisible = useIsElementVisible(lastRef.current);

  useEffect(() => {
    // Fetch no mount da página.
    getMoreTodos(todos.page + 1);
  }, []);

  useEffect(() => {
    // Fetch ao chegar no final da lista.
    if (isLastVisible) {
      getMoreTodos(todos.page + 1);
    }
  }, [isLastVisible]);

  const getMoreTodos = async (page) => {
    try {
      setIsLoading(true);
      const newTodos = await fetchTodos(page);
      setTodos((prev) => ({
        ...newTodos,
        itens: prev.itens.concat(newTodos.itens),
      }));
      setIsLoading(false);
    } catch (err) {}
  };

  return (
    <div>
      <h2>Lorem ipsum's list</h2>
      {todos.itens.map(({ title }, index) => {
        return (
          <div key={index}>
            <p>{title}</p>
          </div>
        );
      })}
      {!!todos.itens.length && <div ref={lastRef} />}
      {isLoading && <p>Loading...</p>}
    </div>
  );
}

export default App;

Conclusão

E está pronto nosso componente com scroll infinito. Eu apenas implementei a listagem e loading dos dados nesse exemplo, fique a vontade de estiliza-los como preferir. No mundo real você também deve tratar a excessão no catch do método getMoreTodos se a API retornar algum erro.

Você pode ver o repositório com todo o código clicando aqui.

Você pode ver a demo ou o repositório com o código.