Issue
Short long story: my component is messed up when React Strict Mode
runs twice useEffect
, and hate to get into bad practices so just don't want to diasable it but don't know how to get over it. I know that is expected behavior, so I'm looking for a solution or alternative.
Long story:
I am trying to adapt a vanilla JS code I found in codepen to make a typing effect on a p
tag, it was quite easy, but then noticed my code was being buggy due to React Strict Mode
because the expected behavior of useEffect
since it worked very well on deployment.
I know there are libraries like TypeIt
that already solved this problem, even in development mode, but I really adapt this one, and now I'm hitting this wall.
Since others already solved this in development mode, there must be a way to do it, but I can't find it, probably I need to rewrite my functions someway.
Here is the code (this component is very simple so you just import it everywhere):
import React, { useEffect, useState } from 'react'
function TestComponent(
{
words,
typeDelay,
eraseDelay,
nextWordDelay,
}
:
{
words: (string)[],
typeDelay: number,
eraseDelay: number,
nextWordDelay: number,
}
)
{
const [text, setText] = useState("");
let wordIndex = 0;
let letterIndex = 0;
const sleep = (delay: number) => new Promise((resolve) => setTimeout(resolve, delay));
const type = async () =>
{
console.log(letterIndex, wordIndex);
if(letterIndex < words[wordIndex].length)
{
const newLetter = words[wordIndex].charAt(letterIndex);
setText(prev => prev + newLetter);
letterIndex++;
await sleep(typeDelay);
type();
return;
}
await sleep(nextWordDelay);
erase();
}
const erase = async () =>
{
if(letterIndex > 0)
{
const newText = words[wordIndex].substring(0, letterIndex - 1);
setText(prev => newText);
letterIndex--;
await sleep(eraseDelay);
erase();
return;
}
wordIndex++;
if(wordIndex >= words.length) wordIndex = 0;
await sleep(nextWordDelay);
type();
}
useEffect(() =>
{
type();
}, [])
return (
<p>
<span className='typed-text'>
{text}
</span>
</p>
)
}
export default TestComponent
I also know there is a thousand posts about useEffect
but found none to ask on how to solve this in development or an alternative to achieve this.
Solution
The easiest way to solve this is to have your useEffect clean up after itself. Right now, when the functions are calling themselves, it will be very hard to clean that up (i.e., stop them from calling each other).
An alternative is to use setInterval. Because with setInterval, it's easy to abort them. Here's an example. I've removed support for multiple words to simplify the example, but you should be able to easily add them back easily.
The useEffect is still being called twice, but because we return a clean up function from the useEffect, React will clear the intervals and you won't notice it ran twice.
import { useCallback, useEffect, useState } from 'react'
type TypingProps = {
word: string,
typeDelay: number,
eraseDelay: number,
nextWordDelay: number,
}
function TestComponent(props: TypingProps)
{
const { word, typeDelay, eraseDelay } = props;
const [textIndex, setTextIndex] = useState(0);
const [isErasing, setIsErasing] = useState(false);
const text = word.slice(0, textIndex);
const type = useCallback(async () =>
{
if(isErasing) { return }
if(textIndex < word.length)
{
setTextIndex(prev => prev + 1)
return;
}
setIsErasing(true)
}, [textIndex, word, isErasing])
const erase = useCallback(async () =>
{
if(!isErasing) { return }
if(textIndex > 0)
{
setTextIndex(prev => prev - 1)
return;
}
setIsErasing(false)
}, [textIndex, isErasing])
useEffect(() =>
{
const typeInterval = setInterval(type, typeDelay);
const eraseInterval = setInterval(erase, eraseDelay);
return () => {
clearInterval(typeInterval);
clearInterval(eraseInterval);
}
}, [eraseDelay, typeDelay, type, erase])
return (
<p>
<span className='typed-text'>
{text}
</span>
</p>
)
}
export default TestComponent
Answered By - Felix Eklöf
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.