DEV Community

Cover image for Como implementar um scroll infinito no React.js criando um clone do TikTok
Ivan Trindade
Ivan Trindade

Posted on

Como implementar um scroll infinito no React.js criando um clone do TikTok

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

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.

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

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);
  }
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
│   └── . . .
└── . . .
Enter fullscreen mode Exit fullscreen mode

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

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;

Enter fullscreen mode Exit fullscreen mode

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:

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;
Enter fullscreen mode Exit fullscreen mode

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:

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

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

* {
  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;
}
Enter fullscreen mode Exit fullscreen mode

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:

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;
Enter fullscreen mode Exit fullscreen mode

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.

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:

root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
Enter fullscreen mode Exit fullscreen mode

Você muda para:

root.render(
    <App />
);
Enter fullscreen mode Exit fullscreen mode

E tudo deve funcionar como esperado.

Adicionando scroll infinito

Para nossa implementação de scroll infinito, usaremos a API nativa do Intersection Observer, 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:

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;
Enter fullscreen mode Exit fullscreen mode

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:

<VideoCard
    key={id}
    index={id + 1}
    lastVideoIndex={videos.length - 1}
    getVideos={getVideos}
/>
Enter fullscreen mode Exit fullscreen mode

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:

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;
Enter fullscreen mode Exit fullscreen mode

Aqui está um detalhamento do que está acontecendo neste componente: Definimos uma ref, elementRef, e a adicionamos ao nosso divscroll-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.

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
Enter fullscreen mode Exit fullscreen mode

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

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;
Enter fullscreen mode Exit fullscreen mode

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:

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;
Enter fullscreen mode Exit fullscreen mode

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.

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!

Top comments (0)