Optimize useTypingEffect timer performance #9

Merged
google-labs-jules[bot] merged 1 commits from perf/optimize-typing-effect-timer-13814216160386825632 into main 2026-01-23 09:55:36 +00:00
Showing only changes of commit 58b82d04c1 - Show all commits

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useMemo } from 'react'; import { useState, useEffect, useMemo, useRef } from 'react';
interface UseTypingEffectOptions { interface UseTypingEffectOptions {
words: string[]; words: string[];
@@ -23,44 +23,60 @@ export function useTypingEffect({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
const stableWords = useMemo(() => words, [JSON.stringify(words)]); const stableWords = useMemo(() => words, [JSON.stringify(words)]);
// Ref to hold the latest state for the timer loop
const stateRef = useRef({ currentText, isDeleting, isPaused });
useEffect(() => {
stateRef.current = { currentText, isDeleting, isPaused };
});
// Effect 1: Handle word switching when deleted
useEffect(() => {
if (isDeleting && currentText === '' && !isPaused) {
setIsDeleting(false);
setCurrentWordIndex((prev) => (prev + 1) % stableWords.length);
}
}, [currentText, isDeleting, isPaused, stableWords]);
// Effect 2: Timer Loop
useEffect(() => { useEffect(() => {
let timer: ReturnType<typeof setTimeout>; let timer: ReturnType<typeof setTimeout>;
const currentWord = stableWords[currentWordIndex]; const currentWord = stableWords[currentWordIndex];
const isWordDeleted = isDeleting && currentText === '';
// If word is fully deleted, move to next word immediately (no timer) const tick = () => {
if (!isPaused && isWordDeleted) { const { currentText, isDeleting, isPaused } = stateRef.current;
setIsDeleting(false);
setCurrentWordIndex((prev) => (prev + 1) % stableWords.length);
return;
}
// Determine speed based on state
const speed = isPaused ? pauseDuration : (isDeleting ? deletingSpeed : typingSpeed);
timer = setTimeout(() => {
if (isPaused) { if (isPaused) {
setIsPaused(false); setIsPaused(false);
setIsDeleting(true); setIsDeleting(true);
} else { return;
// Typing logic }
if (isDeleting) { if (isDeleting) {
if (currentText === '') {
// Handled by the other effect
return;
}
setCurrentText((prev) => prev.substring(0, prev.length - 1)); setCurrentText((prev) => prev.substring(0, prev.length - 1));
timer = setTimeout(tick, deletingSpeed);
} else { } else {
const nextText = currentWord.substring(0, currentText.length + 1); const nextText = currentWord.substring(0, currentText.length + 1);
setCurrentText(nextText); setCurrentText(nextText);
if (nextText === currentWord) { if (nextText === currentWord) {
setIsPaused(true); setIsPaused(true);
} else {
timer = setTimeout(tick, typingSpeed);
} }
} }
} };
}, speed);
// Determine initial speed
const speed = isPaused ? pauseDuration : (isDeleting ? deletingSpeed : typingSpeed);
timer = setTimeout(tick, speed);
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [ }, [
currentText,
isDeleting, isDeleting,
isPaused, isPaused,
currentWordIndex, currentWordIndex,