I try to create a Text Typer component, where text is added character by character every 100ms.
Everything working good on production, but when I try to run it on dev env, impossible to make it work fine 2 intervals are running at the same time whatever the solution I try to implement (final typed text is ADFHJLNPRTVXZ
)
I know that it come from React Strict mode since v18 to catch potential issue, but here it cause issue and I want the same behaviors on every environment.
I tested by addind a clearInterval on top, to stop the first trigger once the second is append. But it’s not workink :/
TextTyper
import React, { Dispatch, useEffect, useRef, useState } from 'react';
export type TextTyperProps = {
text: string,
/** Do not forget to wrap it under `useCallback` to avoid refresh the `useEffect` dependencies */
onTextTyped?: Dispatch<void>,
intervalTime?: number
};
export const TextTyper = ({ text, onTextTyped, intervalTime = 100 }: TextTyperProps) => {
const [textTyped, setTextTyped] = useState('');
const indexRef = useRef(0); // Keep index stored n a ref
const intervalId = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
// Re-init all state when useEffect is called
setTextTyped("");
indexRef.current = 0;
// Clean current interval in case text is udpated before typed completly, or useEffect is runned twice in dev mode
if (intervalId.current) {
clearInterval(intervalId.current);
}
// Select and type the next chat
const typeNextChar = () => {
if (indexRef.current < text.length) {
setTextTyped((currentText) => {
const nextChar = text.charAt(indexRef.current);
console.log(`${indexRef.current}/${text.length} => ${currentText}[${nextChar}] `)
indexRef.current++;
return currentText + nextChar;
});
} else {
// Once the text is completly typed, stop interval
if (intervalId.current) {
clearInterval(intervalId.current);
}
// And propagate
onTextTyped?.();
}
};
// Start a new interval
intervalId.current = setInterval(typeNextChar, intervalTime);
// Clear interval on unmount
return () => {
if (intervalId.current) {
clearInterval(intervalId.current);
}
};
}, [text, onTextTyped, intervalTime]);
return <div>{textTyped}</div>;
};
Test Component
"use client"
import { useCallback, useState } from "react";
import { TextTyper } from "~/components/ui/TextTyper";
export default function TestTextTyper() {
const [text, setText] = useState<string>("");
const [isDone, setDone] = useState<boolean>(false);
// Additional feature to force reload if ask for the same text
const [lastUpdate, setLastUpdate] = useState<number>(0);
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const revered = alphabet.split('').reverse().join('');
const handleClick = (newText: string) => {
setDone(false);
setText(newText);
setLastUpdate(Date.now())
}
const handleTextTyped = useCallback(() => {
console.log("Text typing complete");
setDone(true)
}, []);
return (
<div>
<button onClick={() => { handleClick(alphabet) }}>{alphabet}</button>
<button onClick={() => { handleClick(revered) }}>{revered}</button>
<TextTyper key={lastUpdate} text={text} onTextTyped={handleTextTyped} />
{isDone && "DONE"}
</div>
);
}
When I click on the first button I got those logs (and we see that 2 interval are running on the same time)
TextTyper.tsx:30 0/26 => [A]
TextTyper.tsx:30 1/26 => [B]
TextTyper.tsx:30 2/26 => A[C]
TextTyper.tsx:30 3/26 => A[D]
TextTyper.tsx:30 4/26 => AD[E]
TextTyper.tsx:30 5/26 => AD[F]
TextTyper.tsx:30 6/26 => ADF[G]
TextTyper.tsx:30 7/26 => ADF[H]
TextTyper.tsx:30 8/26 => ADFH[I]
TextTyper.tsx:30 9/26 => ADFH[J]
TextTyper.tsx:30 10/26 => ADFHJ[K]
TextTyper.tsx:30 11/26 => ADFHJ[L]
TextTyper.tsx:30 12/26 => ADFHJL[M]
TextTyper.tsx:30 13/26 => ADFHJL[N]
TextTyper.tsx:30 14/26 => ADFHJLN[O]
TextTyper.tsx:30 15/26 => ADFHJLN[P]
TextTyper.tsx:30 16/26 => ADFHJLNP[Q]
TextTyper.tsx:30 17/26 => ADFHJLNP[R]
TextTyper.tsx:30 18/26 => ADFHJLNPR[S]
TextTyper.tsx:30 19/26 => ADFHJLNPR[T]
TextTyper.tsx:30 20/26 => ADFHJLNPRT[U]
TextTyper.tsx:30 21/26 => ADFHJLNPRT[V]
TextTyper.tsx:30 22/26 => ADFHJLNPRTV[W]
TextTyper.tsx:30 23/26 => ADFHJLNPRTV[X]
TextTyper.tsx:30 24/26 => ADFHJLNPRTVX[Y]
TextTyper.tsx:30 25/26 => ADFHJLNPRTVX[Z]
page.tsx:24 Text typing complete