React custom hooks using refs as index counters 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 &quot;react&quot;;
import useInterval from &quot;./useInterval.js&quot;;

/**
 * A simple typewriter that prints strings, one character after another
 * @param {Array&lt;string&gt;} strings - Array of strings to type
 * @param {function&lt;string&gt;} set - A useState setter to update the current string value
 * @param {number} defaultDelay - Delay between updates
 */
const useTyper = (strings, set, defaultDelay = 100) =&gt; {
  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 = () =&gt; {
    const currentString = strings[stringsIdx.current];

    set(currentString.substr(0, stringIdx.current + 1));

    if (stringIdx.current &gt; currentString.length) {
      // Done with string
      stringsIdx.current += 1;
      stringIdx.current = 0;
      if (stringsIdx.current &gt; 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) =&gt; {
  const savedCallback = React.useRef();

  // Remember the latest callback.
  React.useEffect(() =&gt; {
    savedCallback.current = callback;
  }, [callback]);

  // Set up the interval.
  React.useEffect(() =&gt; {
    function tick() {
      savedCallback.current();
    }
    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () =&gt; {
        clearInterval(id);
      };
    }
  }, [delay]);
};

export default useInterval;

Here is how the useTyper hook can be used.
import React, { useState } from &quot;react&quot;;
import &quot;./styles.css&quot;;
import useTyper from &quot;./hooks/useTyper.js&quot;;
export default function App() {
  const [placeHolder, setPlaceHolder] = useState(&quot;&quot;);

  const placeHolders = [
    &quot;First input suggestion&quot;,
    &quot;Second input suggeston&quot;,
    &quot;Creativity flows...   &quot;,
    &quot;Alright, lets leave it here.&quot;
  ];

  useTyper(placeHolders, setPlaceHolder);

  return (
    &lt;div className=&quot;App&quot;&gt;
      &lt;div className=&quot;container&quot;&gt;
        &lt;input
          id=&quot;formElement&quot;
          autoComplete=&quot;off&quot;
          autoFocus
          size=&quot;20&quot;
          type=&quot;text&quot;
          placeholder={placeHolder}
        /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}

Again, here's the link to the code sandbox.
Looks to be fairly clean code.
Suggestions

Use string::slice or string::substring versus string::substr. substr isn't deprecated but also isn't recommended for use.


Warning: Although String.prototype.substr(…) is not strictly
deprecated (as in &quot;removed from the Web standards&quot;), it is considered
a legacy function and should be avoided when possible. It is not part
of the core JavaScript language and may be removed in the future. If
at all possible, use the substring() method instead.

setCurrentString(currentString.slice(0, currentStringIndex.current + 1));


Provide clearer parameter names, i.e. stringArray versus just strings, and setString or setCurrentString versus set. set alone can be confusing as it isn't very descriptive, i.e. &quot;is it a mathematical set?&quot; By naming a function with verbNoun it is clearer to a reader what it is and what it does.

const useTyper = (stringsArray, setCurrentString, defaultDelay = 100) =&gt; {


stringIdx and stringsIdx can cause mental gymnastics looking for the 's' to differentiate the two. Isolated from one another the names aren't terrible, but when used together within the same function it is more demanding. The names should also align closer to the variable they indicate an index for. Based on the previous parameter naming suggestion I recommend stringArrayIndex and currentStringIndex.
You also appear to do an extra iteration on the currentString with the conditional test currentStringIndex.current &gt; currentString.length. You can increment to the next string when the current string's index is equal to the length, currentStringIndex.current &gt;= currentString.length.
Similar conditional test for checking for the end of the strings array, when the index is. greater then or equal to the length you can quit. stringArrayIndex.current &gt;= stringArray.length

const useTyper = (stringArray, setCurrentString, defaultDelay = 100) =&gt; {
  const [delay, setDelay] = React.useState(defaultDelay);
  // index counters, normally named i and j if this were an imperative loop
  const currentStringIndex = React.useRef(0);
  const stringArrayIndex = React.useRef(0);

  const textTyper = () =&gt; {
    const currentString = stringArray[stringArrayIndex.current];

    setCurrentString(currentString.slice(0, currentStringIndex.current + 1));

    if (currentStringIndex.current &gt;= currentString.length) {
      // Done with string
      stringArrayIndex.current += 1;
      currentStringIndex.current = 0;
      if (stringArrayIndex.current &gt;= stringArray.length) {
        setDelay(null); // Ended.
      }
    }
    currentStringIndex.current += 1;
  };

  useInterval(textTyper, delay);
};

Good job, nice hook.