React: Typewriter Effect

I have a short and sweet little React app to write about this month, which achieves a Typewriter effect. When given a word, this word gets spelled out one letter at a time, with a 500ms delay between each letter.

Let's start with a basic React component:

// components/Typewriter.js
export default function Typewriter({ children: character }) {
  return character;
}

We'll use this in our App like so:

// App.js
import { useState } from "react";
import Typewriter from "./components/Typewriter";

export default function App() {
  const [word, setWord] = useState("Howdy");

  return (
    <div className="App">
      <p>
        {word.split("").map((character, index) => (
          <Typewriter key={index}>
            {character}
          </Typewriter>
        ))}
      </p>
    </div>
  );
}

So we've split the word up into distinct characters and passed them off to the Typewriter component. We can now update Typewriter.js to start showing those characters. The first thing we'll do is use a showCharacter state variable, initially set to false, that corresponds to whether or not that character is shown.

// components/Typewriter.js
import { useState } from "react";

export default function Typewriter({ children: character }) {
  const [showCharacter, setShowCharacter] = useState(false);

  return showCharacter && character;
}

Now we can use a setTimeout function within useEffect to show that character after 500ms:

// components/Typewriter.js
import { useState, useEffect } from "react";

export default function Typewriter({ children: character }) {
  const [showCharacter, setShowCharacter] = useState(false);

  useEffect(() => {
    const timer = setTimeout(() => {
      setShowCharacter(true);
    }, 500);

    return () => {
      clearTimeout(timer);
    };
  }, []);

  return showCharacter && character;
}

So the component lifecycle right now is this: The component mounts and showCharacter is false. After 500ms, showCharacter is set to true, so the character is returned by the component, making it show up in the DOM. When the application is closed and the component unmounts, useEffect runs its cleanup function, removing the setTimeout function.

The problem here is that after 500ms, every letter in the word is shown all at once. What we want is for the first letter to appear after 500ms, the second letter to appear after 1000ms, the third letter to appear after 1500ms, and so on. We can adjust the interval amount by using the index of each letter in the word to achieve this. The formula for doing so is:

(index * 500) + 500

So if index is 0, then we just get 500, if index is 1, we get 1000, if index is 2, we get 1500. This way the letters have a staggered delay. I'll add a stagger prop and adjust the setTimeout function to use this formula.

// components/Typewriter.js
import { useState, useEffect } from "react";

export default function Typewriter({ stagger, children: character }) {
  const [showCharacter, setShowCharacter] = useState(false);

  useEffect(() => {
    const timer = setTimeout(() => {
      setShowCharacter(true);
    }, stagger * 500 + 500);

    return () => {
      clearTimeout(timer);
    };
  }, [stagger]);

  return showCharacter && character;
}

Now the last thing that I need to do is pass that stagger prop with the index value. Back in App.js:

// App.js
import { useState } from "react";
import Typewriter from "./components/Typewriter";

export default function App() {
  const [word, setWord] = useState("Howdy");

  return (
    <div className="App">
      <p>
        {word.split("").map((character, index) => (
          <Typewriter key={index} stagger={index}>
            {character}
          </Typewriter>
        ))}
      </p>
    </div>
  );
}

That's it! Short and sweet, very simple.