fix: cancel nested setTimeout in useTypewriter, add empty words guard

This commit is contained in:
MBO-Tech-IT 2026-04-02 01:19:27 +02:00
parent 2d1f21efe7
commit a33115e56d
1 changed files with 17 additions and 3 deletions

View File

@ -1,20 +1,31 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useRef, useState } from "react";
/**
* Cycles through `words` with a typewriter effect.
* @param words - Must be referentially stable (define outside component or wrap in useMemo).
* @param typingSpeed - ms per character while typing (default 80)
* @param deletingSpeed - ms per character while deleting (default 40)
* @param pauseMs - ms to pause after a full word is typed (default 2000)
* @returns The currently displayed string
*/
export function useTypewriter(words: string[], typingSpeed = 80, deletingSpeed = 40, pauseMs = 2000) { export function useTypewriter(words: string[], typingSpeed = 80, deletingSpeed = 40, pauseMs = 2000) {
const [displayed, setDisplayed] = useState(""); const [displayed, setDisplayed] = useState("");
const [wordIndex, setWordIndex] = useState(0); const [wordIndex, setWordIndex] = useState(0);
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
const pauseRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => { useEffect(() => {
if (!words.length) return;
const current = words[wordIndex % words.length]; const current = words[wordIndex % words.length];
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
if (!isDeleting) { if (!isDeleting) {
setDisplayed(current.slice(0, displayed.length + 1)); setDisplayed(current.slice(0, displayed.length + 1));
if (displayed.length + 1 === current.length) { if (displayed.length + 1 === current.length) {
setTimeout(() => setIsDeleting(true), pauseMs); pauseRef.current = setTimeout(() => setIsDeleting(true), pauseMs);
} }
} else { } else {
setDisplayed(current.slice(0, displayed.length - 1)); setDisplayed(current.slice(0, displayed.length - 1));
@ -25,7 +36,10 @@ export function useTypewriter(words: string[], typingSpeed = 80, deletingSpeed =
} }
}, isDeleting ? deletingSpeed : typingSpeed); }, isDeleting ? deletingSpeed : typingSpeed);
return () => clearTimeout(timeout); return () => {
clearTimeout(timeout);
if (pauseRef.current) clearTimeout(pauseRef.current);
};
}, [displayed, isDeleting, wordIndex, words, typingSpeed, deletingSpeed, pauseMs]); }, [displayed, isDeleting, wordIndex, words, typingSpeed, deletingSpeed, pauseMs]);
return displayed; return displayed;