My custom hook useTyper.js ended up as a bit of a mess and hard to read.
It's mimicking a nested for-loop but using React.useRefs to hold on to the indexes between renders. The indexes are the current positions of the outer and inner loop, those often named i and j in traditional nested for loops.
Here's a fancy code sandbox for the mess below.
One thing I'm particularly unhappy with is the naming of stringIdx and stringsIdx. Any suggestion for a change?
useTyper.js
import React from "react";
import useInterval from "./useInterval.js";
/**
 * A simple typewriter that prints strings, one character after another
 * @param {Array<string>} strings - Array of strings to type
 * @param {function<string>} set - A useState setter to update the current string value
 * @param {number} defaultDelay - Delay between updates
 */
const useTyper = (strings, set, defaultDelay = 100) => {
  const [delay, setDelay] = React.useState(defaultDelay);
  // index counters, normally named i and j if this were an imperative loop
  const stringIdx = React.useRef(0);
  const stringsIdx = React.useRef(0);
  const textTyper = () => {
    const currentString = strings[stringsIdx.current];
    set(currentString.substr(0, stringIdx.current + 1));
    if (stringIdx.current > currentString.length) {
      // Done with string
      stringsIdx.current += 1;
      stringIdx.current = 0;
      if (stringsIdx.current > strings.length - 1) {
        setDelay(null); // Ended.
      }
    }
    stringIdx.current += 1;
  };
  useInterval(textTyper, delay);
};
export default useTyper;
For completion, this is the useInterval hook being used by useTyper.
It can be referenced from one of Dan Abramovs blog posts.
useInterval.js
import React from 'react';
const useInterval = (callback, delay) => {
  const savedCallback = React.useRef();
  // Remember the latest callback.
  React.useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);
  // Set up the interval.
  React.useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => {
        clearInterval(id);
      };
    }
  }, [delay]);
};
export default useInterval;
Here is how the useTyper hook can be used.
import React, { useState } from "react";
import "./styles.css";
import useTyper from "./hooks/useTyper.js";
export default function App() {
  const [placeHolder, setPlaceHolder] = useState("");
  const placeHolders = [
    "First input suggestion",
    "Second input suggeston",
    "Creativity flows...   ",
    "Alright, lets leave it here."
  ];
  useTyper(placeHolders, setPlaceHolder);
  return (
    <div className="App">
      <div className="container">
        <input
          id="formElement"
          autoComplete="off"
          autoFocus
          size="20"
          type="text"
          placeholder={placeHolder}
        />
      </div>
    </div>
  );
}
Again, here's the link to the code sandbox.
