Building a Loading Component with ChatGPT

Ruben Casas - Jan 28 '23 - - Dev Community

Have you ever seen the red progress bar on Youtube or the blue progress bar on GitHub when navigating from one page to the next?

youtube loading bar

github progress bar

I like that little component; it's elegant and subtle and perfectly fits with Remix and React Router transitions.

So I decided to build my own

That was easy

Here are the rules:

  • I am really lazy! Can ChatGPT write for me?
  • I want to use React, no vanilla JS
  • No external dependencies (duh)
  • It needs to support animations

Here we go! 💪

ChatGPT first attempt:

Prompt:

"Write a react component that creates a red bar 3px in height with a dynamic width that starts at 0% and start incrementing every 10ms and dissapears when it reaches 100% width with a fade out animation. Reply with code blocks"

import React, { useState, useEffect } from 'react';
import './ProgressBar.css';

const ProgressBar = () => {
  const [width, setWidth] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setWidth(prevWidth => (prevWidth >= 100 ? 100 : prevWidth + 1));
    }, 10);

    return () => clearInterval(intervalId);
  }, []);

  return (
    <div
      className={`progress-bar ${width === 100 ? 'fade-out' : ''}`}
      style={{ width: `${width}%`, height: '3px' }}
    />
  );
};

export default ProgressBar;
Enter fullscreen mode Exit fullscreen mode
.progress-bar {
  background-color: red;
}

.fade-out {
  animation: fade-out 0.5s;
}

@keyframes fade-out {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}
Enter fullscreen mode Exit fullscreen mode

That was a good start!

loader

 Next Prompt:

"Now, I want the progress bar to conditional render and mount an unmount when a condition is true or false, when it is unmounted it should fill the width to 100% and then fade out and disappear."

export const ProgressBar2 = ({ show }) => {
  const [width, setWidth] = useState(0);
  const [isAnimating, setIsAnimating] = useState(false);

  useEffect(() => {
    let intervalId;
    if (show && !isAnimating) {
      setIsAnimating(true);
      intervalId = setInterval(() => {
        setWidth((prevWidth) => {
          if (prevWidth >= 100) {
            return prevWidth;
          }
          return prevWidth + 1;
        });
      }, 10);
    }
    return () => clearInterval(intervalId);
  }, [show, isAnimating]);

  useEffect(() => {
    if (!show && width === 100) {
      setTimeout(() => {
        setWidth(0);
        setIsAnimating(false);
      }, 500);
    }
  }, [show, width]);

  if (!show) {
    return null;
  }

  return (
    <div
      className={`progress-bar ${width === 100 ? 'fade-out' : ''}`}
      style={{
        width: `${width}%`,
        height: '3px',
        position: 'fixed',
        top: 0,
        left: 0,
      }}
    />
  );
};
Enter fullscreen mode Exit fullscreen mode

Prompt:
"What? That doesn't work at all! the bar never appears!"

ChatGPT:
I apologize for the confusion, it looks like my previous response did not include the necessary CSS for the animation.

...

What CSS? That code doesn't even make sense!

...

Many prompts later... an increasing list of useEffect functions, and a very apologetic ChatGPT, I decided to give up and build it myself.

Boy, that was a big mistake!

Random trickle:

The width increments were gradual every 10ms, but I want to add some randomness to the bar progress to give the impression that some non-deterministic async operation is actually happening... (Seriously, the bar is an illusion! It doesn't have anything to do with how long the operation takes 😂)

const Bar = (props: ProgressBarProps) => {
  const [width, setWidth] = useState(0);
  const progressSpeed = 10;

  useEffect(() => {
    const interval = setInterval(() => {
      setWidth((prevValue: number) => {
        const random = Math.random() / 100;
        if (prevValue + random >= 0.95) {
          clearInterval(interval);
          return prevValue;
        }
        return prevValue + random;
      });
    }, progressSpeed);

    return () => clearInterval(interval);
  }, [progressSpeed]);
...
Enter fullscreen mode Exit fullscreen mode

That was easy wasn't it?

 React Animations

You know what's not easy? Animations in React

Animations in React

It turns out that Animations in React are a pain!

When the component is unmounted, I want to fill the bar to 100% then show a fade animation.

In good old JQuery is a one-liner, as simple as:

$('#progress-bar').fadeOut(300, function(){ $(this).remove();});
Enter fullscreen mode Exit fullscreen mode

Oh, but not React; React wants you to suffer! (unless you use a library like react-transition-group, of course.

But the rules are the rules, and I said no libraries... 😅

So after consulting with ChatGPT's arch enemy and more reliable predecessor, StackOverflow. I found that you can wrap your component in a container and use a method called onAnimationEnd to detect when the animation finishes and then hide the element.

So here is the result:

import React, { useEffect, useState } from 'react';
import './progress-bar.css';

interface ProgressBarProps {
  show: boolean;
}

export const ProgressBar = (props: ProgressBarProps) => {
  const { show } = props;
  const [shouldRender, setRender] = useState(show);

  useEffect(() => {
    if (show) setRender(true);
  }, [show]);

  const onAnimationEnd = () => {
    if (!show) {
      setRender(false);
    }
  };

  return (
    shouldRender && (
      <div
        aria-label="progress-bar-container"
        style={{
          animation: `${show ? '' : 'fadeOut 0.5s'}`,
        }}
        onAnimationEnd={onAnimationEnd}
      >
        <Bar {...props} show={show} />
      </div>
    )
  );
};

const Bar = (props: ProgressBarProps) => {
  const { show } = props;
  const [width, setWidth] = useState(0);
  const progressSpeed = 10;

  useEffect(() => {
    const interval = setInterval(() => {
      setWidth((prevValue: number) => {
        const random = Math.random() / 100;
        if (prevValue + random >= 0.95) {
          clearInterval(interval);
          return prevValue;
        }
        return prevValue + random;
      });
    }, progressSpeed);

    return () => clearInterval(interval);
  }, [progressSpeed, show]);

  return (
    <div
      aria-label="progress-bar"
      style={{
        position: 'fixed',
        top: 0,
        left: 0,
        zIndex: 2147483647,
        backgroundColor: '#FF0000',
        height: '3px',
        width: `${10 + width * 90}%`,
        transition: `width ${progressSpeed}ms`,
        transform: 'translate3d(0, 0, 0)',
      }}
    />
  );
};
Enter fullscreen mode Exit fullscreen mode

It works!

loading bar

Except it doesn't! There is an annoying "flashing glitch" when the component unmounts that I couldn't be bothered finding out why (I am lying; I spent way too long trying but then gave up)

 Conclusion

Use nProgress 😂

That's it!

 Real Conclusion:

This was a fun exercise to stretch the limits of ChatGPT and how far it can go in creating entire components and UI. It did very well with the basics, but when you start adding more complex interactions, it just goes round and round in circles spitting out incoherent and repetitive code. (not without apologising, of course 😅)

Worth noting that I couldn't make the component work 100% either!, mainly due to lack of time (or lack of React expertise 😂).

If anyone wants to judge my midnight coding and fix the bug, and let me know why the glitch happens, here is the link to the code:
https://stackblitz.com/edit/github-9eintn-ru5xtl?file=src%2Fprogress-bar.tsx

 My Conclusion:

I need to get better at React animations.

. . . . . . . . . . . . . . . . . . . . .