Como implementar um scroll infinito no React.js criando um clone do TikTok
Tags: react, tutorial, javascript, braziliandevs
Published: 2023-01-19 18:35:30

Em 2006,  Aza Raskin introduziu  o conceito de scroll infinito, uma funcionalidade que mais tarde transformaria nossas vidas digitais. Facebook, TikTok, AliExpress e Pinterest, para citar alguns, implementaram o recurso de scroll infinito em suas histórias ou feeds.

O scroll infinito traz essencialmente informações e entretenimento infinitos para sua tela. Você continua rolando e rolando, mas nunca chega ao fim. Ao longo dos anos, recebeu muitas críticas,  até mesmo de seu criador , devido à sua natureza viciante. Independentemente disso, essa tecnologia é fascinante tanto do ponto de vista comportamental quanto da implementação.

Este artigo abordará como o scroll infinito funciona nos bastidores e como o TikTok incorpora o scroll infinito. Também criaremos um clone do TikTok com React.js, CSS puro e vídeos gratuitos da API do Pexels para entender melhor o processo de implementação.

Para ser claro, implementaremos principalmente a versão móvel do feed do TikTok, em vez de criar um clone completo do TikTok. Aqui está uma prévia de como ficará nossa aplicação:


![Scroll infinito](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3gbrxoadjd8eeh2vwy7k.gif)

## Como funciona o scroll infinito

As implementações do scroll inifito variam entre as aplicações, mas a ideia por trás de como todos funcionam é praticamente a mesma. Todos contam com programação assíncrona e uma API para carregar o conteúdo.

Primeiro, você faz que sua aplicação busque algum conteúdo de vídeo assim que a aplicação é carregada. Você também adiciona um ouvinte que assistequando o usuário rola a tela ou quando o vídeo termina. Quando o ouvinte é acionado, a aplicação carrega o novo conteúdo de forma assíncrona por meio da API.


![lustração demonstrando como funciona a rolagem infinita, com três itens carregados inicialmente, além da legenda “ o usuário está aqui ”, que aponta para uma seta representando o carregamento de um novo conteúdo de um banco de dados ou API.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8v0k0ssm66nhxkyw05rs.png)

Um pseudocódigo semelhante ao JavaScript simples para esse processo pode ser assim:

```jsx
function getContent(n) {
  // Asynchronously load 'n' new post from API
}

window.addEventListener("DOMContentLoaded", (event) => {
  getContent(5);
});

window.addEventListener("scroll", (e) => {
  if (e.scrollY === somePreferredScrollFrame) {
    getContent(5);
  }
});
```

## Como funciona o TikTok

O feed do TikTok funciona praticamente da mesma maneira descrita acima; no entanto, o conteúdo no caso do TikTok são vídeos criados ou compartilhados por outros usuários da aplicação. Além disso, o TikTok incorpora um recurso exclusivo em que cada vídeo é exibido em altura total, separando-os um do outro, juntamente com um efeito de rolagem suave que torna a rolagem de cada vídeo satisfatória.

Replicar recursos especiais como esse é bastante simples com CSS puro. Utilizaremos as propriedades `scroll-snap` e `scroll-behavior` para o efeito de rolagem. Implementaremos totalmente outros métodos para fazer o efeito de scroll infinito funcionar com JavaScript nativo.

## Como construir um clone do TikTok

Vamos começar criando uma nova aplicação React. Para fazer isso, certifique-se de ter npx( Node.js ) instalado e execute o seguinte comando:

```
npx create-react-app tiktok-clone
```

Em seguida, abra a pasta projetada recém-criada em seu editor de texto favorito e execute o seguinte comando para iniciar a aplicação em seu navegador:

```
npm start
```

## Implementando scroll-snap de altura total e barra de navegação inferior

Vamos continuar criando os componentes scroll-snap e barra de navegação inferior semelhantes ao TikTok. Para fazer isso, crie um novo diretório `components` na pasta `/src` existente e crie dois novos arquivos: `BottomNav.js` e `VideoCard.js` nesse novo diretório. Com essas alterações, nossa árvore de arquivos deve ficar assim:

```
.
├── . . .
├── public
├── src
│   ├── components
│   │   ├── BottomNav.js
│   │   └── VideoCard.js
│   └── . . .
└── . . .
```

Dentro do arquivo `VideoCard.js`, cole o código abaixo:

```jsx
const VideoCard = ({ index }) => {
  return (
    <div className="slider-children">
      <div
        style={{
          justifyContent: "center",
          alignItems: "center",
          display: "flex",
          height: "100%",
        }}
      >
        <h1>Video {index}</h1>
      </div>
    </div>
  );
};

export default VideoCard;

```

O código acima cria um novo componente `VideoCard` que aceita uma única prop chamada index e exibe esse índice concatenado com a palavra “Vídeo”. É importante ressaltar que esse componente também contém a marcação e os nomes de classe que permitem o efeito de ajuste de rolagem de altura total.

Dentro do arquivo `BottomNav.js`, adicione o seguinte conteúdo:

```jsx
const BottomNav = () => {
  return (
    <nav className="bottom-nav">
      <a className="navbar-brand" href="/">
        <i className="fa fa-home"></i>
      </a>
      <a className="navbar-brand" href="/">
        <i className="fa fa-search"></i>
      </a>
      <a className="navbar-brand" href="/">
        <i className="fa fa-plus"></i>
      </a>
      <a className="navbar-brand" href="/">
        <i className="fa fa-commenting"></i>
      </a>
      <a className="navbar-brand" href="/">
        <i className="fa fa-user"></i>
      </a>
    </nav>
  );
};

export default BottomNav;
```

O código acima contém a marcação, nomes de classe e definições de ícone para nossa barra de navegação inferior semelhante ao TikTok. Usamos  ícones Font Awesome  para os ícones, como visto no código anterior, mas para que funcione, também precisaremos vincular seu arquivo de recurso. Para fazer isso, abra o arquivo `public/index.html` e cole o seguinte na seção head:

```jsx
<link
  rel="stylesheet"
  href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"
/>
```

Para classificar todos os códigos relacionados a CSS, substitua o conteúdo do arquivo `src/index.css` padrão pelo seguinte:

```jsx
* {
  padding: 0;
  margin: 0;
  box-sizing: border-box;
}

*::-webkit-scrollbar {
  display: none;
}

html,
body {
  height: 100vh;
  overflow: hidden;
  color: #fff;
  font-family: 'Helvetica Neue', sans-serif;
}

.slider-container {
  height: 100vh;
  overflow-y: scroll;
  scroll-snap-type: y mandatory;
  scroll-behavior: smooth;
}

.slider-children {
  height: 100vh;
  scroll-snap-align: start;
  background: #000;
  position: relative;
  border: 1px solid transparent;
}

.video {
  position: absolute;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.video-content {
  padding: 10px;
  position: relative;
  top: 85%;
  color: #fff;
}

.bottom-nav {
  position: fixed;
  right: 0;
  bottom: 0;
  left: 0;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20px;
  background-color: #000;
}

.bottom-nav a {
  color: #fff;
  text-decoration: none;
}

.fa {
  font-size: 20px;
}

.fa-plus {
  color: #000;
  background: #fff;
  padding: 3px 10px;
  border-radius: 10px;
  border: 2px solid #ff5722c4;
}
```

Como você deve ter notado, esse código CSS contém o estilo para obter o efeito de rolagem suave, fixando nossa barra de navegação na parte inferior da página, um elemento de vídeo que adicionaremos no futuro e um estilo adicional.

Finalmente, para encerrar esta seção, vamos importar os componentes recém-criados em nosso arquivo de entrada. Abra `src/App.js` e substitua seu código pelo abaixo:

```jsx
import { useState, useEffect } from "react";

import BottomNav from "./components/BottomNav";
import VideoCard from "./components/VideoCard";

function App() {
  const [videos, setvideos] = useState([]);

  const getVideos = (length) => {
    let newVideos = Array.from(Array(length).keys());
    setvideos((oldVideos) => [...oldVideos, ...newVideos]);
  };

  useEffect(() => {
    getVideos(3);
  }, []);

  return (
    <main>
      <div className="slider-container">
        {videos.length > 0 ? (
          <>
            {videos.map((video, id) => (
              <VideoCard key={id} index={id + 1} />
            ))}
          </>
        ) : (
          <>
            <h1>Nothing to show here</h1>
          </>
        )}
      </div>

      <BottomNav />
    </main>
  );
}

export default App;
```

Aqui, importamos os componentes `BottomNav` e `VideoCard` que criamos anteriormente. Também definimos um estado `videos` usando  useState do React  e definimos seu valor inicial como um array vazio. Além disso, definimos uma função `getVideos()`, que recebe um comprimento, cria um array desse comprimento e empurra os valores resultantes para o nosso estado `videos` definido anteriormente.

Além disso, usamos o hook useEffect para chamar a função `getVideos()` assim que nossa aplicação foi montada, fazendo com que ele adicionasse três novos itens ao nosso estado `videos`. E em nossa marcação, fizemos um loop videos, renderizando o componente `VideoCard` para cada iteração enquanto também transmitíamos o id da iteração (índice) como a propriedade de índice para nosso componente `VideoCard`.

Se visualizarmos nossa aplicação neste ponto, veremos a seguinte saída:


![A tela de um telefone celular mostra o usuário percorrendo três blocos de texto diferentes, cada um com altura total e texto branco sobre fundo preto.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tvix5eyp3nbnwyfd3xoe.gif)

Suponha que sua aplicação renderize seis partes de conteúdo em vez de três depois de concluir essas etapas. Nesse caso, isso ocorre porque o React  StrictMode  renderiza os componentes duas vezes porque estamos no modo de desenvolvimento – um dos impactos da  atualização para o React 18.

Para corrigir isso, abra `src/index.js` e remova a opção StrictMode. Em vez de retornar:

```jsx
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
```
Você muda para:

```jsx
root.render(
    <App />
);
```
E tudo deve funcionar como esperado.

## Adicionando scroll infinito

Para nossa implementação de scroll infinito, usaremos a API nativa do  [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API), que nos permite observar de forma assíncrona a visibilidade de um elemento na janela de visualização do navegador.

Nossa abordagem para alcançar o scroll infinito, será nas seguintes etapas:

1. Crie um hook customizado no viewport (através da API Intersection Observer) para verificar se um elemento está atualmente no viewport.

2. Passe um comprimento preferencial para o componente `VideoCard` no qual pretendemos carregar o novo conteúdo. Por exemplo: após o usuário rolar até 3/5 da tela (neste caso, comprimento é 3), carregar x novo conteúdo.

3. Verificar se rolamos para o comprimento preferido definido na etapa 2 usando Reac Ref.

4. Usando a condição da etapa anterior, carregue o novo conteúdo x e aumente o comprimento preferido para um comprimento futuro para o qual ainda não rolamos.

Para realizar a primeira etapa, crie um novo arquivo `useIsInViewport.js` no diretório `/src` existente e cole o seguinte código nele:

```jsx
import { useEffect, useState, useMemo } from "react";

function useIsInViewport(ref) {
  const [isIntersecting, setIsIntersecting] = useState(false);

  const observer = useMemo(
    () =>
      new IntersectionObserver(([entry]) =>
        setIsIntersecting(entry.isIntersecting)
      ),
    []
  );

  useEffect(() => {
    observer.observe(ref.current);

    return () => {
      observer.disconnect();
    };
  }, [ref, observer]);

  return isIntersecting;
}

export default useIsInViewport;
```

Aqui criamos um hook personalizado,` useIsInViewport`, que aceita uma referência como seu parâmetro e usa a API `Intersection Observer` para verificar se o elemento com a referência fornecida está atualmente na viewport.

Para a segunda etapa, altere o arquivo `src/App.js` de forma que o componente VideoCard adicione uma nova entrada de props como esta:

```jsx
<VideoCard
    key={id}
    index={id + 1}
    lastVideoIndex={videos.length - 1}
    getVideos={getVideos}
/>
```

A única diferença aqui é que passamos um novo parâmetro, `lastVideoIndex`, e definimos seu valor para a duração de nossos vídeos originais (3) – 1. Dessa forma, inicialmente carregamos três vídeos e desejamos carregar novos conteúdos quando o usuário atingir dois dos três vídeos. Também passamos a função `getVideos()` como parâmetro para que possamos chamá-la diretamente do componente `VideoCard`.

A mudança final acontece em nosso arquivo componente do `VideoCard`. Abra `src/components/VideoCard.js` e atualize seu código com o seguinte:

```jsx
import { useRef, useState } from "react";

import useIsInViewport from "../useIsInViewport";

const VideoCard = ({ index, lastVideoIndex, getVideos }) => {
  const elementRef = useRef();
  const isInViewport = useIsInViewport(elementRef);
  const [loadNewVidsAt, setloadNewVidsAt] = useState(lastVideoIndex);

  if (isInViewport) {
    if (loadNewVidsAt === Number(elementRef.current.id)) {
      // increase loadNewVidsAt by 2
      setloadNewVidsAt((prev) => prev + 2);
      getVideos(3);
    }
  }

  return (
    <div className="slider-children">
      <div
        ref={elementRef}
        id={index}
        style={{
          justifyContent: "center",
          alignItems: "center",
          display: "flex",
          height: "100%",
        }}
      >
        <h1>Video {index}</h1>
      </div>
    </div>
  );
};

export default VideoCard;
```

Aqui está um detalhamento do que está acontecendo neste componente: Definimos uma ref, elementRef, e a adicionamos ao nosso div` scroll-snap`. Também demos a essa div um novo id e definimos seu valor para corresponder ao índice do conteúdo atual. Em seguida, passamos o `elementRef` para o hook `useIsInViewport` para verificar se ele está atualmente na viewport.

Além disso, usando useState do React, definimos um novo estado `loadNewVidsAt` e definimos seu valor inicial para a prop `lastVideoIndex` que passamos anteriormente. Estamos fazendo isso para tornar a atualização desse valor mais flexível, já que não podemos alterar diretamente uma prop. 

Por fim, usamos uma instrução `if` para verificar se um vídeo está na viewport e, se estiver, verificamos se o id desse vídeo atual é igual ao valor do estado `loadNewVidsAt` (ou seja, o valor no qual queremos carregar novos conteúdos). Se essa condição for atendida, carregamos três novos vídeos usando `getVideos(3)` e definimos o estado `loadNewVidsAt` para seu valor anterior + 2.

E voilá! Implementamos com sucesso o scroll infinito. Se você rodar sua aplicação neste ponto, tudo deve funcionar conforme o esperado, conforme a imagem abaixo:


![A tela de um telefone celular mostra o usuário percorrendo blocos de texto infinitos, cada um com altura total e texto branco sobre fundo preto.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rcffcxr64ssg11yjuhep.gif)

No entanto, até agora, temos apenas texto infinito, não vídeos infinitos. Vamos nos aprofundar na adição de vídeos na próxima seção.

## Atualize com arquivos de vídeo de amostra

Como nosso clone do TikTok não oferece suporte a nenhuma publicação de conteúdo, carregaremos vídeos por meio de uma API de terceiros. Pesquisei para você e descobri que o  Pexels  fornece uma API acessível e excelente para carregar vídeos e imagens da comunidade. A API do Pexels é flexível, pois podemos especificar a duração dos vídeos que queremos recuperar e filtrá-los por diferentes categorias.

O que você quer fazer agora é ir para  a página inicial do Pexels  e criar uma nova conta. Depois de verificar sua conta, acesse a  página da API  e solicite uma nova API. Depois de seguir as instruções destacadas nesta página, você deve obter instantaneamente sua chave de API. Copie isso e guarde-o em um lugar seguro por enquanto.

Em seguida, queremos instalar a biblioteca `Pexels` em nossa aplicação. Para fazer isso, execute o seguinte comando:

```
npm install pexels
```

Feito isso, abra `src/App.js `e substitua seu conteúdo pelo seguinte código:

```jsx
import { useState, useEffect } from "react";

import { createClient } from "pexels";

import BottomNav from "./components/BottomNav";
import VideoCard from "./components/VideoCard";

function App() {
  const [videos, setvideos] = useState([]);
  const [videosLoaded, setvideosLoaded] = useState(false);

  const randomQuery = () => {
    const queries = ["Funny", "Art", "Animals", "Coding", "Space"];
    return queries[Math.floor(Math.random() * queries.length)];
  };

  const getVideos = (length) => {
    // Replace with your Pexels API Key
    const client = createClient("YOUR_PEXEL_API_KEY");

    const query = randomQuery();
    client.videos
      .search({ query, per_page: length })
      .then((result) => {
        setvideos((oldVideos) => [...oldVideos, ...result.videos]);
        setvideosLoaded(true);
      })
      .catch((e) => setvideosLoaded(false));
  };

  useEffect(() => {
    getVideos(3);
  }, []);

  return (
    <main>
      <div className="slider-container">
        {videos.length > 0 ? (
          <>
            {videos.map((video, id) => (
              <VideoCard
                key={id}
                index={id}
                author={video.user.name}
                videoURL={video.video_files[0].link}
                authorLink={video.user.url}
                lastVideoIndex={videos.length - 1}
                getVideos={getVideos}
              />
            ))}
          </>
        ) : (
          <>
            <h1>Nothing to show here</h1>
          </>
        )}
      </div>

      <BottomNav />
    </main>
  );
}

export default App;
```

As principais modificações neste arquivo são que atualizamos a função `getVideos()` para agora realmente carregar arquivos de vídeo através da API Pexels, e também adicionamos uma nova função `randomQuery()` que gera consultas aleatórias e nos permite passar a consulta gerada para nossa API Pexels solicitar. 

Além disso, alteramos a inicialização do componente `VideoCard`  para incluir informações sobre os vídeos carregados, como autor do vídeo, URL do vídeo e um link para o perfil do criador.

Por fim, atualize `src/components/VideoCard.js` e também substitua o conteúdo deste arquivo pelo código abaixo:

```jsx
import { useRef, useState, useEffect } from "react";

import useIsInViewport from "../useIsInViewport";

const VideoCard = ({
  index,
  author,
  videoURL,
  authorLink,
  lastVideoIndex,
  getVideos,
}) => {
  const video = useRef();
  const isInViewport = useIsInViewport(video);
  const [loadNewVidsAt, setloadNewVidsAt] = useState(lastVideoIndex);

  if (isInViewport) {
    setTimeout(() => {
      video.current.play();
    }, 1000);

    if (loadNewVidsAt === Number(video.current.id)) {
      setloadNewVidsAt((prev) => prev + 2);
      getVideos(3);
    }
  }

  const togglePlay = () => {
    let currentVideo = video.current;
    if (currentVideo.paused) {
      currentVideo.play();
    } else {
      currentVideo.pause();
    }
  };

  useEffect(() => {
    if (!isInViewport) {
      video.current.pause();
    }
  }, [isInViewport]);

  return (
    <div className="slider-children">
      <video
        muted
        className="video"
        ref={video}
        onClick={togglePlay}
        id={index}
        autoPlay={index === 1}
      >
        <source src={videoURL} type="video/mp4" />
      </video>
      <div className="video-content" onClick={togglePlay}>
        <p>@{author}</p>
        <p>
          Video by <a href={authorLink}>{author} </a> on Pexel
        </p>
      </div>
    </div>
  );
};

export default VideoCard;
```

As principais modificações a este arquivo são que adicionamos uma marcação de vídeo e definimos sua fonte para aquela carregada da API do Pexels. Também usamos nosso hook `useIsInViewport` personalizado para verificar o vídeo na viewport e reproduzi-lo automaticamente. Se o vídeo não estiver sendo reproduzido, usamos o hook `useEffect` para interrompê-lo.

Além disso, criamos uma nova função `togglePlay()`, semelhante ao TikTok, que permite ao usuário pausar ou reproduzir um vídeo a qualquer momento, simplesmente clicando em qualquer lugar dentro do quadro do vídeo.

E agora nosso clone do TikTok está completo! Quando executamos nosso programa agora, obtemos os seguintes resultados:


![A tela de um telefone celular exibe vídeos curtos de pessoas dançando, paisagens urbanas e decoração de parede. Os vídeos mudam quando o usuário rola para cima.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2io607efp0y964ckb2cg.gif)

Ao longo desse artigo, exploramos como o scroll infinito funciona e como implementar o feed de notícias de scroll infinito do TikTok, carregando vídeos gratuitos da API Pexels. Obrigado por ler!