1

I am working on typewriter effect in React JS. I have previously implemented typewriter effect in Vanilla JS.

The React JS code that I am writing is

import React, { useEffect, useRef, useState } from 'react'

const Typewriter = () => {

    const el = useRef();
    const words = ["Life", "Could Be", "A Dream"];

    const [index, setIndex] = useState(0);
    const [subIndex, setSubIndex] = useState(0);
    const [isEnd, setIsEnd] = useState(false);
    const [isDeleting, setIsDeleting] = useState(false);
    const [duration, setDuration] = useState(200);
    const [currentWord, setCurrentWord] = useState("");
    const spedUp = 50;
    const normalSpeed = 200;

    useEffect(() => {
        const interval = setInterval(() => {

            setIndex(index % words.length);
            setIsEnd(false);

            if (!isDeleting && subIndex <= words[index].length) {
                setCurrentWord((prev) => prev.concat(words[index][subIndex]));
                setSubIndex((prev) => prev += 1);
                el.current.innerHTML = currentWord;
            }

            if (isDeleting && subIndex > 0) {
                setCurrentWord((prev) => prev.substring(0, prev.length - 1));
                setSubIndex((prev) => prev -= 1);
                el.current.innerHTML = currentWord;
            }

            if (subIndex === words[index].length) {
                setIsEnd(true);
                setIsDeleting(true);
            }

            if (subIndex === 0 && isDeleting) {
                setCurrentWord("");
                setIsDeleting(false);
                setIndex((prev) => prev += 1);
            }

            setDuration(isEnd ? 1500 : isDeleting ? spedUp : normalSpeed);
        }, duration);

        return () => clearInterval(interval);
    }, [subIndex, index, currentWord, isEnd, isDeleting, duration, words])

    return (
        <>
            <h1 ref={el}>Placeholder text</h1>
        </>
    )
}

export default Typewriter

When I run the React App, the phrases/words are appended with 'undefined' at the end. The application displays the 3 phrases once and then the app crashes because the code tries to get the length of an undefined array element.

Here are the visuals

React Application GIF

Can someone explain to me what is happening and are the dependencies used for useEffect hook correct or not?

General Grievance
  • 4,555
  • 31
  • 31
  • 45
Anirudh Giran
  • 55
  • 1
  • 6

1 Answers1

0

Main issue

You are using set(X) and making an assumption that the value will be immediately set and that you can base later conditions on it. useState does not actually set the state immediately -- it happens on the next loop, so any conditions you have (like testing the index after setIndex) are going to go awry because they're using stale values. See useState set method not reflecting change immediately

I avoided this by using interim values (newIndex, newSubIndex) -- I'm not wild about this approach -- I'd probably refactor with a different strategy, but it was the quickest way to get this up and running.

import React, { useEffect, useRef, useState } from 'react'

const Typewriter = () => {

    const words = ["Life", "Could Be", "A Dream"];

    const [index, setIndex] = useState(0);
    const [subIndex, setSubIndex] = useState(0);
    const [isEnd, setIsEnd] = useState(false);
    const [isDeleting, setIsDeleting] = useState(false);
    const [duration, setDuration] = useState(200);
    const [currentWord, setCurrentWord] = useState("");
    const spedUp = 50;
    const normalSpeed = 200;

    useEffect(() => {
        const interval = setInterval(() => {

          let newSubIndex = subIndex;
          let newIndex = index % words.length;

          setIsEnd(false);

            if (!isDeleting && subIndex < words[newIndex].length) {
                setCurrentWord((prev) => prev.concat(words[newIndex][subIndex]));
                newSubIndex += 1;
            }

            if (isDeleting && subIndex > 0) {
                setCurrentWord((prev) => prev.substring(0, prev.length - 1));
                newSubIndex -= 1;
            }

            if (newSubIndex === words[newIndex].length) {
                setIsEnd(true);
                setIsDeleting(true);
            }

            if (newSubIndex === 0 && isDeleting) {
                setCurrentWord("");
                setIsDeleting(false);
                newIndex += 1;
            }

            setSubIndex(newSubIndex);
            setIndex(newIndex);

            setDuration(isEnd ? 1500 : isDeleting ? spedUp : normalSpeed);
        }, duration);

        return () => clearInterval(interval);
    }, [subIndex, index, currentWord, isEnd, isDeleting, duration, words])

    return (
        <>
            <h1>{currentWord}</h1>
        </>
    )
}

export default Typewriter

Minor issue

In React, rather than using a ref and editing innerHTML, normally the content is just inlined in the rendered DOM structure. See <h1>{currentWord}</h1>


You may want to look into storing your state in a larger object rather than everything being a separate useState. You may also want to look into useReducer.

jnpdx
  • 45,847
  • 6
  • 64
  • 94
  • Thank you for the quick response. I tried the solution that you suggested. It worked just fine but now the issue coming is that when the whole word is written. The typewriter should wait for some time. But instead, the typewriter is deleting the last character of the word and then waiting for the specified time. **Visual**: https://imgur.com/a/hbVo6mL – Anirudh Giran Mar 28 '21 at 02:39
  • My guess is that you'll have to set a temporary variable for deleting -- the issue is probably `setIsDeleting(true)` and then checking for it in the next clause. – jnpdx Mar 28 '21 at 04:18