fix: cancel nested setTimeout in useTypewriter, add empty words guard
This commit is contained in:
parent
2d1f21efe7
commit
a33115e56d
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue