skip to Main Content

I am implementing a typewriter effect in my React app where I fetch a string from a URL and display it one character at a time. I expect the effect to start from the first character (index 0) and proceed through all the characters sequentially. However, the second character (index 1) is skipped, and the rest of the characters are displayed correctly.

Current behaviour:
enter image description here

Expected behaviour:enter image description here

Additional Information:
The text state is updated correctly after fetching the data.
I’ve added a 500ms delay between the display of each character.

Any suggestions on improving the logic or debugging this issue would be appreciated!

import "./styles.css";
import React, { useEffect, useState } from "react";

export default function App() {
  const [text, setText] = useState(""); // To hold the fetched text
  const [displayText, setDisplayText] = useState(""); // For the typewriter effect
  const [loading, setLoading] = useState(true); // Loading state

  // Fetching text from the URL
  useEffect(() => {
    const fetchText = async () => {
      try {
        const response = await fetch("https://example-url.com/text"); // Fetch from URL
        const data = await response.text();
        setText(data); // Save the text in the state
        setLoading(false); // Loading done
      } catch (error) {
        console.error("Error fetching the text:", error);
        setLoading(false);
      }
    };

    fetchText();
  }, []);

  // Typewriter effect logic

  useEffect(() => {
    if (!loading && text) {
      let index = 0;
      const interval = setInterval(() => {
        setDisplayText((prevText) => prevText + text[index]);
        index++;
        if (index === text.length) {
          clearInterval(interval); // Stop when all characters are shown
        }
      }, 500); // 500ms delay between characters

      return () => clearInterval(interval); // Cleanup
    }
  }, [text, loading]);

  // Rendering the text with a typewriter effect
  return (
    <div className="App">
      {loading ? (
        <p>Loading...</p> // Loading text
      ) : (
        <ul>
          {displayText.split("").map((char, index) => (
            <li key={index}>{char}</li> // Render each character in a list
          ))}
        </ul>
      )}
    </div>
  );
}

I used setInterval in useEffect to append characters one by one from the fetched string. I expected the typewriter effect to start from the first character (index 0), but it skips the second one and displays rest correctly.

2

Answers


  1. Issue

    The issue here is that the effect/interval callback is mutating the index before the state update is processed. React state updates are enqueued and then processed when the function scope and callstack are completed.

    let index = 0;
    
    const interval = setInterval(() => {
      setDisplayText((prevText) => prevText + text[index]); // <--called later
      index++; // <-- mutated now!
    
      if (index === text.length) {
        clearInterval(interval);
      }
    }, 500);
    

    index is mutated to 1 before the first state update is processed to use the text[0] value.

    Solution

    I suggest storing the index in state and incrementing that instead of copying parts of the text state into the displayText state. You can compute the derived "display text" value from the text and index states.

    Example Implementation:

    export default function App() {
      const [text, setText] = useState("");
      const [index, setIndex] = useState(0);
      const [loading, setLoading] = useState(true);
    
      const timerRef = useRef();
    
      // Fetching text from the URL
      useEffect(() => {
        const fetchText = async () => {
          try {
            const response = await fetch("https://example-url.com/text"); // Fetch from URL
            const data = await response.text();
            const data = "uncloak";
            setText(data);
          } catch (error) {
            console.error("Error fetching the text:", error);
          } finally {
            setLoading(false);
          }
        };
    
        fetchText();
      }, []);
    
      // Typewriter effect logic
      useEffect(() => {
        let index = 0;
        if (!timerRef.current && text) {
          timerRef.current = setInterval(() => {
            setIndex((i) => i + 1);
          }, 500); // 500ms delay between characters
    
          // Cleanup
          return () => {
            clearInterval(timerRef.current);
            timerRef.current = null;
          };
        }
      }, [text]);
    
      useEffect(() => {
        if (timerRef.current && index === text.length) {
          clearInterval(timerRef.current);
          timerRef.current = null;
        }
      }, [index, text]);
    
      // Rendering the text with a typewriter effect
      return (
        <div className="App">
          {loading ? (
            <p>Loading...</p> // Loading text
          ) : (
            <ul>
              {text
                .split("")
                .slice(0, index)
                .map((char, index) => (
                  <li key={index}>{char}</li> // Render each character in a list
                ))}
            </ul>
          )}
        </div>
      );
    }
    

    Demo:

    function App() {
      const [text, setText] = React.useState(""); // To hold the fetched text
      const [index, setIndex] = React.useState(0);
      const [loading, setLoading] = React.useState(true); // Loading state
    
      const timerRef = React.useRef();
    
      // Fetching text from the URL
      React.useEffect(() => {
        const fetchText = async () => {
          try {
            await new Promise((resolve) => {
              setTimeout(resolve, 4000);
            });
            const data = "uncloak";
            setText(data); // Save the text in the state
          } catch (error) {
            console.error("Error fetching the text:", error);
          } finally {
            setLoading(false); // Loading done
          }
        };
    
        fetchText();
      }, []);
    
      // Typewriter effect logic
      React.useEffect(() => {
        let index = 0;
        if (!timerRef.current && text) {
          timerRef.current = setInterval(() => {
            setIndex((i) => i + 1);
          }, 500); // 500ms delay between characters
    
          // Cleanup
          return () => {
            clearInterval(timerRef.current);
            timerRef.current = null;
          };
        }
      }, [text]);
    
      React.useEffect(() => {
        if (timerRef.current && index === text.length) {
          clearInterval(timerRef.current);
          timerRef.current = null;
        }
      }, [index, text]);
    
      // Rendering the text with a typewriter effect
      return (
        <div className="App">
          {loading ? (
            <p>Loading...</p> // Loading text
          ) : (
            <ul>
              {text
                .split("")
                .slice(0, index)
                .map((char, index) => (
                  <li key={index}>{char}</li> // Render each character in a list
                ))}
            </ul>
          )}
        </div>
      );
    }
    
    const rootElement = document.getElementById("root");
    const root = ReactDOM.createRoot(rootElement);
    
    root.render(
      <React.StrictMode>
        <App />
      </React.StrictMode>
    );
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
    <div id="root" />
    Login or Signup to reply.
  2. I have revised your code to use a timeout instead of an interval, and I track the index with a ref. I have revised your code so we can test by setting the string to be displayed manually

    import React, { useEffect, useState, useRef } from 'react';
    
    export function App() {
      const [text, setText] = useState(''); // To hold the fetched text
      const [displayText, setDisplayText] = useState([]); // For the typewriter effect
      const inputRef = useRef();
      const indexRef = useRef(0);
    
      // Fetching text from the URL
    
      // Typewriter effect logic
    
      useEffect(() => {
        if (text.length === 0 || displayText.length === text.length) return;
        const timeout = setTimeout(() => {
          setDisplayText(p => [...p, text[indexRef.current]]);
        }, 500);
        return () => {
          clearTimeout(timeout);
          indexRef.current += 1;
        };
      }, [text, displayText]);
    
      function set() {
        console.dir(inputRef.current.value);
        indexRef.current = 0;
        setText(inputRef.current.value);
        setDisplayText([])
      }
    
      // Rendering the text with a typewriter effect
      return (
        <div className='App'>
          <input ref={inputRef} />
          <button onClick={set}>set</button>
          <ul>
            {displayText.map((char, index) => (
              <li key={index}>{char}</li> // Render each character in a list
            ))}
          </ul>
        </div>
      );
    }
    

    I’m not 100% certain what your issue was.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search