OneGraph / essay.dev

Create an issue on this repo to publish to your very own blog at <your-github-username>.essay.dev
https://essay.dev
8 stars 1 forks source link

Build a Loading Spinner that Just Won’t Quit #1

Open dwwoelfel opened 4 years ago

dwwoelfel commented 4 years ago

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:

<iframe src="https://codesandbox.io/embed/jittery-spinner-63vvk?fontsize=14&hidenavigation=1&theme=dark&view=preview" style="width:100%; height:300px; border:0; border-radius: 4px; overflow:hidden;" title="jittery-spinner"

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:

<iframe src="https://codesandbox.io/embed/wont-quit-spinner-zpf9l?fontsize=14&hidenavigation=1&theme=dark" style="width:100%; height:300px; border:0; border-radius: 4px; overflow:hidden;" title="wont-quit-spinner"

You can also try it out at Source Code Shots[^1].

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

[^1]: The code highlighting in this very blog post is powered by source code shots

{"authors": [{"name": "Daniel", "url": "https://twitter.com/danielwoelfel", "avatarUrl": "https://avatars0.githubusercontent.com/u/476818?s=96&u=8902611617e1833d27ce6e32f06f6db5113c60aa&v=4"}]}
dwwoelfel commented 1 year ago

Test