Skip to content
Advertisement

setTimeout Function Not Removing Characters Like A Queue

This is a simple for loop that runs 80 times but only every 100 ms. Each time it runs, it pushes a Y coordinate for a character into the stillChars state array. The stillChars state array is mapped through to create a sequence of text elements directly underneath eachother.

See this video to see the animation

const StillChars = () => {
  const [stillChars, setStillChars] = useState([]);

  function addChar() {
    for (let i = 0; i < 80; i++) {
      setTimeout(() => {
        setStillChars((pS) => [
          ...pS,
          {
            y: 4 - 0.1 * 1 * i,
            death: setTimeout(() => {
              setStillChars((pS) => pS.filter((c, idx) => idx !== i));
            }, 2000),
          },
        ]);
      }, i * 100);
    }
  }

  useEffect(() => {
    addChar();
  }, []);

  return (
    <>
      {stillChars.map((c, i) => {
        return <StillChar key={i} position={[0, c.y, 0]} i={i} />;
      })}
    </>
  );
};

Code Sandbox

Desired behavior: The death: setTimeout function should remove the characters like a queue instead of whatever is going on in the video/codesandbox.

Advertisement

Answer

Your main mistake is with:

setStillChars((pS) => pS.filter((c, idx) => idx !== i))

The problem is that this sits within a setTimeout() so the above line runs at different times for each iteration that your for loop does. When it runs the first time, you update your state to remove an item, which ends up causing the elements in your array that were positioned after the removed item to shift back an index. Eventually, you’ll be trying to remove values for i that no longer exist in your state array because they’ve all shifted back to lower indexes. One fix is to instead associate the index with the object itself by creating an id. This way, you’re no longer relying on the position to work out which object to remove, but instead, are using the object’s id, which won’t change when you filter out items from your state:

{
  id: i, // add an `id`
  y: 4 - 0.1 * 1 * i,
  death: setTimeout(() => {
    setStillChars((pS) => pS.filter(c => c.id !== i)); // remove the item based on the `id`
  }, 2000),
},

Along with this change, you should now change the key within your .map() to use the object’s id and not its index, otherwise, the character won’t update in your UI if you’re using the same index to represent different objects:

return <StillChar key={c.id} position={[0, c.y, 0]} i={i} />;

As also highlighted by @KcH in the question comments, you should also remove your timeouts when your component unmounts. You can do this by returning a cleanup function from your useEffect() that calls clearTimeout() for each timeout that you create. In order to reference each timeout, you would need to store this in an array somewhere, which you can do by creating a ref with useRef(). You may also consider looking into using setInterval() instead of a for loop.

User contributions licensed under: CC BY-SA
9 People found this is helpful
Advertisement