Build a Loading Spinner that Just Won’t Quit

I have a project, source code shots, that turns your source code into a png as you type. It takes a little bit of time to generate the image, so I wanted to indicate to the user that some work was being done to give them a visual cue that the image on the page was going to update.

I made a simple loading spinner component, using a css animation, and rendered it while the image was being fetched.

The code looked something like

/* styles.css */
@keyframes throb {
  0% {
    transform: scale(0.1, 0.1);
    opacity: 0;
  }
  50% {
    opacity: 0.8;
  }
  100% {
    transform: scale(1.2, 1.2);
    opacity: 0;
  }
}
.loading {
  animation: throb 1s ease-out;
  animation-iteration-count: infinite;
  opacity: 0;
  border: 3px solid #999;
  border-radius: 30px;
  height: 16px;
  width: 16px;
}
export default function App() {
  const [isLoading, setIsLoading] = React.useState(false);
  const [code, setCode] = React.useState("");
  React.useEffect(() => {
    let canceled = false;
    setIsLoading(true);
    // Simulate getting the image for the code
    new Promise((resolve) =>
      setTimeout(resolve, Math.random() * 300 + 100)
    ).then(() => {
      if (!canceled) {
        setIsLoading(false);
      }
    });
    return () => {
      canceled = true;
    };
  }, [code, setIsLoading]);

  return (
    <div>
      <textarea value={code} onChange={(e) => setCode(e.target.value)} />
      <div
        className="loading"
        style={{ display: isLoading ? "block" : "none" }}
      />
    </div>
  );
}

I thought I was done, but the spinner was so annoying I almost threw it out. As I typed, the animation would start and stop, creating an annoying jittery effect.

Type a few characters at a time into the textarea below to see what I mean:

What I would like is for the animation to continue to completion, even if we’re finished loading. That way the animation will never flash in and out.

A little google searching reveals a family of animation hooks that look like they could be useful. There is an onanimationiteration event, accessible with the onAnimationIteration React prop on our loading div, that will fire on every round of the animation. In our case, it will fire every second.

Instead of hiding our loading spinner when isLoading switches to false, we can wait for the onAnimationIteration hook to fire and hide the loading spinner in the callback.

The relevant change looks like

      <div
        className="loading"
        onAnimationIteration={() => {
          if (!isLoading) {
            setPlayAnimation(false);
          }
        }}
        style={{ display: playAnimation ? "block" : "none" }}
      />

Try it below:

You can also try it out at Source Code Shots1.

Do you have a better way to build an elegant loading spinner? Let me know in the comments below.