0

I'm building a web application and have a function that types out letters one by one for effect, at 100ms intervals. However it's skipping the second letter of the string I'm trying to have it type.

It works if I prepend two spaces at the beginning of the string, but I have no idea why.

useEffect(() => {
  let message = 'This is not working';
  let initialIndex = 0;
  setText('');

  const typingInterval = setInterval(() => {
    if (initialIndex < message.length-1) {
      setText((prevText) => prevText + message[initialIndex]);
      initialIndex++;
    } else {
      clearInterval(typingInterval);
    }
  }, 100);
jeremy radcliff
  • 1,049
  • 3
  • 11
  • 27
  • 1
    Please post the complete code of the `useEffect` call, in particular the cleanup function and dependencies. Also include the `useState` that `setText` is presumably coming from – Bergi Jul 12 '23 at 00:15
  • And add the [tag:reactjs] tag while you're at it, please. – Darryl Noakes Jul 12 '23 at 00:41

2 Answers2

2

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);
}, []);
Thomas
  • 11,958
  • 1
  • 14
  • 23
  • 1
    Instead of `const char = message[index++]`, just do `const char = message[text.length]` inside the callback – Bergi Jul 12 '23 at 00:18
0

setState method is asynchronous, so initialIndex++ runs before your setText and it skips the index number 0.

Solution

https://github.com/the-road-to-learn-react/use-state-with-callback#usage

Ali Navidi
  • 693
  • 1
  • 5
  • 18