Merge pull request #5 from ragusa-it/fix-typing-effect-timer-leak-7224626563395361239

Refactor useTypingEffect to fix timer leak and optimize re-renders
This commit was merged in pull request #5.
This commit is contained in:
Melvin Ragusa
2026-01-23 10:06:33 +01:00
committed by GitHub

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useMemo } from 'react';
interface UseTypingEffectOptions { interface UseTypingEffectOptions {
words: string[]; words: string[];
@@ -18,38 +18,45 @@ export function useTypingEffect({
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
const [isPaused, setIsPaused] = useState(false); const [isPaused, setIsPaused] = useState(false);
// Stabilize words array to prevent unnecessary effect resets on re-renders
// when the array reference changes but content remains the same.
// eslint-disable-next-line react-hooks/exhaustive-deps
const stableWords = useMemo(() => words, [JSON.stringify(words)]);
useEffect(() => { useEffect(() => {
let timer: ReturnType<typeof setTimeout>; let timer: ReturnType<typeof setTimeout>;
if (isPaused) { const currentWord = stableWords[currentWordIndex];
timer = setTimeout(() => { const isWordDeleted = isDeleting && currentText === '';
// If word is fully deleted, move to next word immediately (no timer)
if (!isPaused && isWordDeleted) {
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) {
setIsPaused(false); setIsPaused(false);
setIsDeleting(true); setIsDeleting(true);
}, pauseDuration);
} else {
const currentWord = words[currentWordIndex];
const isWordDeleted = isDeleting && currentText === '';
if (isWordDeleted) {
setIsDeleting(false);
setCurrentWordIndex((prev) => (prev + 1) % words.length);
} else { } else {
const speed = isDeleting ? deletingSpeed : typingSpeed; // Typing logic
if (isDeleting) {
setCurrentText((prev) => prev.substring(0, prev.length - 1));
} else {
const nextText = currentWord.substring(0, currentText.length + 1);
setCurrentText(nextText);
timer = setTimeout(() => { if (nextText === currentWord) {
if (isDeleting) { setIsPaused(true);
setCurrentText((prev) => prev.substring(0, prev.length - 1));
} else {
const nextText = currentWord.substring(0, currentText.length + 1);
setCurrentText(nextText);
if (nextText === currentWord) {
setIsPaused(true);
}
} }
}, speed); }
} }
} }, speed);
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [ }, [
@@ -57,7 +64,7 @@ export function useTypingEffect({
isDeleting, isDeleting,
isPaused, isPaused,
currentWordIndex, currentWordIndex,
words, stableWords,
typingSpeed, typingSpeed,
deletingSpeed, deletingSpeed,
pauseDuration, pauseDuration,