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.
73 lines
1.7 KiB
TypeScript
73 lines
1.7 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
|
|
interface UseTypingEffectOptions {
|
|
words: string[];
|
|
typingSpeed?: number;
|
|
deletingSpeed?: number;
|
|
pauseDuration?: number;
|
|
}
|
|
|
|
export function useTypingEffect({
|
|
words,
|
|
typingSpeed = 100,
|
|
deletingSpeed = 50,
|
|
pauseDuration = 2000,
|
|
}: UseTypingEffectOptions) {
|
|
const [currentWordIndex, setCurrentWordIndex] = useState(0);
|
|
const [currentText, setCurrentText] = useState('');
|
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
const [isPaused, setIsPaused] = useState(false);
|
|
|
|
useEffect(() => {
|
|
let timer: ReturnType<typeof setTimeout>;
|
|
|
|
if (isPaused) {
|
|
timer = setTimeout(() => {
|
|
setIsPaused(false);
|
|
setIsDeleting(true);
|
|
}, pauseDuration);
|
|
} else {
|
|
const currentWord = words[currentWordIndex];
|
|
const isWordDeleted = isDeleting && currentText === '';
|
|
|
|
if (isWordDeleted) {
|
|
setIsDeleting(false);
|
|
setCurrentWordIndex((prev) => (prev + 1) % words.length);
|
|
} 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);
|
|
}
|
|
}
|
|
|
|
return () => clearTimeout(timer);
|
|
}, [
|
|
currentText,
|
|
isDeleting,
|
|
isPaused,
|
|
currentWordIndex,
|
|
words,
|
|
typingSpeed,
|
|
deletingSpeed,
|
|
pauseDuration,
|
|
]);
|
|
|
|
return {
|
|
text: currentText,
|
|
isTyping: !isDeleting && !isPaused,
|
|
isDeleting,
|
|
currentWordIndex,
|
|
};
|
|
}
|