skip to Main Content

I’m making a game about pokemon in React and there’s a component called Announcer which announces what is happening in it.

I made the letters appear one by one just like old games and I wanted that this behavior would happen one message at a time, but React is rendering all messages simultaneously:

https://i.sstatic.net/IYLLcYTW.gif

Here’s the code of the Announcer and AnnouncerMessage

import AnnouncerMessage from "./AnnouncerMessage";

const Announcer = ({ messages }) => {
  return (
    <div className="announcer">
      <div className="announcerMessages">
        {messages.map((msg) => (
          <AnnouncerMessage message={msg} />
        ))}
      </div>
    </div>
  );
};

export default Announcer;

import { useState, useEffect, useRef } from "react";

const AnnouncerMessage = ({ message }) => {
  const [placeholder, setPlaceholder] = useState("");

  const index = useRef(1);

  useEffect(() => {
    const tick = () => {
      setPlaceholder(message.slice(0, index.current));
      index.current++;
    };
    if (index.current <= message.length) {
      setTimeout(tick, 25);
    }
  }, [placeholder]);

  return <p>{placeholder}</p>;
};

export default AnnouncerMessage;

2

Answers


  1. What happens in your code essentially is each AnnouncerMessagecomponent starts its “letter-by-lteer animation” as soon as it’s rendered.

    This happens because each timeout you define in each AnnouncerMessagecomponent is independent from the others.

    One way of solving this, is simply to have a “global” counter (in the StackBlitz example below currentMessageIndex) which keeps tract of which component is rendering.

    Then it’s just a matter of defining a callback function (handleMessageComplete in the example) which increments the global counter.

    Additionally, you need to have a dedicate props (isCurrent in the example) to keep track whether the current component is being render or was rendered before.
    We need this to disable the animation on the components already rendered, since React will re-render each child component once a state update happens on the parent component.

    Hope this helps, below you can find the StackBlitz repo with the full example.

    Announcer

    const Announcer = ({ messages }) => {
      const [currentMessageIndex, setCurrentMessageIndex] = useState(0);
    
      const handleMessageComplete = () => {
        setCurrentMessageIndex((prevIndex) =>
          prevIndex < messages.length - 1 ? prevIndex + 1 : prevIndex
        );
      };
    
      return (
        <div className="announcer">
          <div className="announcerMessages">
            {messages.slice(0, currentMessageIndex + 1).map((msg, index) => (
              <AnnouncerMessage
                key={index}
                message={msg}
                isCurrent={index === currentMessageIndex}
                onComplete={handleMessageComplete}
              />
            ))}
          </div>
        </div>
      );
    };
    

    AnnouncerMessage

    const AnnouncerMessage = ({ message, isCurrent, onComplete }) => {
      const [displayedText, setDisplayedText] = useState('');
    
      useEffect(() => {
        let timeoutId;
    
        if (isCurrent) {
          let index = 0;
    
          const tick = () => {
            index++;
            setDisplayedText(message.slice(0, index));
            if (index < message.length) {
              timeoutId = setTimeout(tick, 35);
            } else {
              if (onComplete) {
                onComplete();
              }
            }
          };
    
          tick();
        } else {
          setDisplayedText(message);
        }
    
        return () => {
          clearTimeout(timeoutId);
        };
      }, [isCurrent, message, onComplete]);
    
      return <p>{displayedText}</p>;
    };
    

    StackBlitz Repo

    Login or Signup to reply.
  2. The issue with your implementation is that all the AnnouncerMessage components are rendering simultaneously because each one starts its animation immediately upon being mounted. To resolve this you need to control when each message starts rendering. A common approach is to use a state to keep track of the current message being displayed and only render one message at a time.

    import { useState, useEffect } from "react";
    import AnnouncerMessage from "./AnnouncerMessage";
    
    const Announcer = ({ messages }) => {
      const [currentMessageIndex, setCurrentMessageIndex] = useState(0);
    
      useEffect(() => {
        if (currentMessageIndex < messages.length) {
          const timeout = setTimeout(() => {
            setCurrentMessageIndex((prevIndex) => prevIndex + 1);
          }, messages[currentMessageIndex].length * 25 + 500); // Time for message + delay
          return () => clearTimeout(timeout);
        }
      }, [currentMessageIndex, messages]);
    
      return (
        <div className="announcer">
          <div className="announcerMessages">
            {messages.slice(0, currentMessageIndex + 1).map((msg, index) => (
              <AnnouncerMessage key={index} message={msg} />
            ))}
          </div>
        </div>
      );
    };
    
    export default Announcer;
    
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search