Quando navegamos em sites com apelo visual — portfólios, landing pages, hero sections — é comum vermos efeitos de texto digitando automaticamente, como uma máquina de escrever. Embora pareça simples, muitos exemplos ainda usam setTimeout
ou setInterval
para isso.
Mas se buscamos algo mais preciso, performático e responsivo, o requestAnimationFrame
é o caminho certo.
O problema de setTimeout
ou setInterval
Essas abordagens funcionam, mas têm sérias desvantagens:
- Não são sincronizadas com o render do navegador
- Continuam rodando mesmo se a aba estiver em segundo plano
- Consomem mais CPU em animações longas
- Podem criar efeitos instáveis se o usuário mudar de aba ou se a aplicação for pesada
Usar requestAnimationFrame
é o caminho
O requestAnimationFrame
é uma API nativa do navegador feita para isso. Ele:
- Executa o código antes de cada frame ser renderizado
- Suspende automaticamente quando a aba fica em segundo plano
- Garante maior precisão temporal e fluidez
- É a base de qualquer animação moderna, como
Framer Motion
,GSAP
, etc.
O hook useTypeWriter
com React
Vamos criar um hook chamado useTypeWriter
, que:
- Digita e apaga uma lista de frases
- Permite configurar a velocidade de digitação e remoção
- Suporta loop e pausas entre as frases
- Usa apenas React e
requestAnimationFrame
(sem libs externas)
import { useEffect, useRef, useState } from "react"; interface UseTypeWriterProvider { texts: string[]; writeSpeed?: number; eraseSpeed?: number; pauseBeforeDelete?: number; pauseBetweenPhrases?: number; loop?: boolean; onCycleComplete?: () => void; } export function useTypeWriter({ texts, writeSpeed = 100, eraseSpeed = 50, pauseBeforeDelete = 1000, pauseBetweenPhrases = 500, loop = false, onCycleComplete = () => { } }: UseTypeWriterProvider) { const [displayed, setDisplayed] = useState(""); // Current visible text // Refs to control typing state without causing re-renders const animationFrameRef = useRef<number | null>(null); // ID from requestAnimationFrame const lastFrameTimeRef = useRef<number>(0); // Timestamp of the last frame const charIndexRef = useRef<number>(0); // Current character index const phraseIndexRef = useRef<number>(0); // Current phrase index const isDeletingRef = useRef<boolean>(false); // Whether we are currently deleting const pauseUntilRef = useRef<number | null>(null); // Pause between transitions useEffect(() => { let isCancelled = false; // Cancel any ongoing animation before starting a new one if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current); const step = (time: number) => { if (isCancelled) return; const currentText = texts[phraseIndexRef.current] || ""; // Handle pauses (e.g., between typing and deleting) if (pauseUntilRef.current && time < pauseUntilRef.current) { animationFrameRef.current = requestAnimationFrame(step); return; }; const delta = time - lastFrameTimeRef.current; const speed = isDeletingRef.current ? eraseSpeed : writeSpeed; // Continue only if the delay time has passed if (delta >= speed) { if (!isDeletingRef.current) { // Typing mode charIndexRef.current = Math.min(charIndexRef.current + 1, currentText.length); setDisplayed(currentText.slice(0, charIndexRef.current)); // Reached the end of the phrase if (charIndexRef.current >= currentText.length) { isDeletingRef.current = true; pauseUntilRef.current = time + pauseBeforeDelete; // Wait before deleting } } else { // Deleting mode charIndexRef.current -= 1; setDisplayed(currentText.slice(0, charIndexRef.current)); // Finished deleting if (charIndexRef.current <= 0) { isDeletingRef.current = false; const nextIndex = phraseIndexRef.current + 1; if (nextIndex >= texts.length) { if (loop) { phraseIndexRef.current = 0; } else { onCycleComplete?.(); // Notify parent return; // Stop animation } } else { phraseIndexRef.current = nextIndex; } charIndexRef.current = 0; pauseUntilRef.current = time + pauseBetweenPhrases; // Pause briefly before typing the next phrase } } lastFrameTimeRef.current = time; // Update last action time } // Keep animation going animationFrameRef.current = requestAnimationFrame(step); }; // Start animation loop animationFrameRef.current = requestAnimationFrame(step); // Clean up on unmount or re-run return () => { isCancelled = true; if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } }; }, [writeSpeed, eraseSpeed, loop, texts, onCycleComplete, pauseBeforeDelete, pauseBetweenPhrases]); return displayed }
Exemplos de uso
import { useTypeWriter } from "./hooks" export function App() { const text = useTypeWriter({ texts: ["Hello, world!", "Welcome to my site."], writeSpeed: 100, eraseSpeed: 50, loop: true }) return ( <main className="bg-zinc-900 w-full h-screen flex items-center justify-center"> <span className="text-zinc-100 text-7xl">{text}</span> </main> ) }
Conclusão
Esse hook é muito bom para:
- Hero sections de portfólios
- Interfaces de onboarding
- Aplicações com personalidade e movimento E sem depender de libs externas.
Curtiu o hook? Veja o código no GitHub e compartilhe com alguém que curte animações em React:
github.com/evenilson/use-typewriter
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.