This is basically the same problem as JS used to have here, (before the introduction of let
and const
)
for (var i=0; i<5; ++i) {
setTimeout(() => console.log(i), 10);
}
by the time console.log(i)
is executed by setTimeout, the loop has already progressed ++i
to the end of the loop.
Same happens in your code, by the time prevText + message[initialIndex]
is called, the next iteration of setinterval
has already been called and executed another initialIndex++;
This happens because setText
is async and React decides when to execute the callback function. And if there is enough load React may delay that call.
To fix this you need a locally scoped variable that stores the index just for this one iterations:
useEffect(() => {
let message = 'This is not working';
let index = 0;
setText('');
const typingInterval = setInterval(() => {
if (index < message.length) {
const char = message[index++]; // this part runs immediately
setText(text => text + char); // this, when React is in the mood for it.
} else {
clearInterval(typingInterval);
}
}, 100);
return () => clearInterval(typingInterval);
}, []);
It works if I prepend two spaces at the beginning of the string, but I have no idea why.
this is purely by chance, the same as the error itself. This "solution" may or may not work depending on if/how React decides to delay the execution of the callback passed to setTimeout
.
Bergis comment:
Instead of const char = message[index++]
, just do const char = message[text.length]
inside the callback
just to clarify this, this would look like this:
useEffect(() => {
let message = 'This is not working';
let index = 0;
setText('');
const typingInterval = setInterval(() => {
if (index < message.length) {
setText(text => text + message[text.length]);
} else {
clearInterval(typingInterval);
}
}, 100);
return () => clearInterval(typingInterval);
}, []);
Actually, you don't need to attach the message char by char:
useEffect(() => {
let message = 'This is not working';
let index = 0;
setText('');
const typingInterval = setInterval(() => {
if (index < message.length) {
setText(message.slice(0, ++index));
} else {
clearInterval(typingInterval);
}
}, 100);
return () => clearInterval(typingInterval);
}, []);