Fix uncleaned setTimeout in useTypingEffect hook

Refactor useTypingEffect to use a single useEffect with proper cleanup for all timers, preventing state updates on unmounted components.
Add unit tests to verify behavior and ensure no memory leaks on unmount.
This commit is contained in:
google-labs-jules[bot]
2026-01-22 04:43:55 +00:00
parent eea2e71b03
commit 8f820e262f
4 changed files with 866 additions and 25 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect } from 'react';
interface UseTypingEffectOptions {
words: string[];
@@ -18,38 +18,50 @@ export function useTypingEffect({
const [isDeleting, setIsDeleting] = useState(false);
const [isPaused, setIsPaused] = useState(false);
const tick = useCallback(() => {
const currentWord = words[currentWordIndex];
useEffect(() => {
let timer: ReturnType<typeof setTimeout>;
if (isPaused) {
return;
}
timer = setTimeout(() => {
setIsPaused(false);
setIsDeleting(true);
}, pauseDuration);
} else {
const currentWord = words[currentWordIndex];
const isWordDeleted = isDeleting && currentText === '';
if (isDeleting) {
setCurrentText(currentWord.substring(0, currentText.length - 1));
if (currentText.length === 0) {
if (isWordDeleted) {
setIsDeleting(false);
setCurrentWordIndex((prev) => (prev + 1) % words.length);
}
} else {
setCurrentText(currentWord.substring(0, currentText.length + 1));
if (currentText === currentWord) {
setIsPaused(true);
setTimeout(() => {
setIsPaused(false);
setIsDeleting(true);
}, pauseDuration);
} else {
const speed = isDeleting ? deletingSpeed : typingSpeed;
timer = setTimeout(() => {
if (isDeleting) {
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);
}
}
}, [currentText, currentWordIndex, isDeleting, isPaused, words, pauseDuration]);
useEffect(() => {
const speed = isDeleting ? deletingSpeed : typingSpeed;
const timer = setTimeout(tick, speed);
return () => clearTimeout(timer);
}, [tick, isDeleting, typingSpeed, deletingSpeed]);
}, [
currentText,
isDeleting,
isPaused,
currentWordIndex,
words,
typingSpeed,
deletingSpeed,
pauseDuration,
]);
return {
text: currentText,