Como implementar um scroll infinito no React.js criando um clone do TikTok
Ivan Trindade
Posted on January 19, 2023
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:
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.
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);
}
});
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:
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:
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:
<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:
* {
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:
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:
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>
);
Você muda para:
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, 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:
Crie um hook customizado no viewport (através da API Intersection Observer) para verificar se um elemento está atualmente no viewport.
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.Verificar se rolamos para o comprimento preferido definido na etapa 2 usando Reac Ref.
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;
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}
/>
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;
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:
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:
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:
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:
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!
Posted on January 19, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.