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
2
Answers
The issue you’re facing is likely due to the fact that the useEffect hook is running twice in development mode due to React Strict Mode
You don’t need an effect here to be honest.
This is my attempt at it. I have replaced the effect in favour of starting the interval during render. The logic remains the same.