skip to Main Content

I created a simple reproduction of my code. Im having an issue with managing an animation of progress bar. The issue is that after pausing an animation you can see the animation goes a little bit backwards and is not being paused in the place it should be when clicking a button. Where is the issue in my code?

And worth mentioning is that its only a reproduction and would be good if class removal is not touched cause in the real code i need to restart the animation on certain condition.

const { useState, useEffect } = React;


const App = () => {
  const totalTime = 10;
  const [timeLeft, setTimeLeft] = useState(totalTime);
  const [isTimerAnimated, setIsTimerAnimated] = useState(true);

  useEffect(() => {
    const interval = setInterval(() => {
      if (timeLeft > 0 && isTimerAnimated) {
        setTimeLeft((prevTimeLeft) => prevTimeLeft - 1);
      }
    }, 1000);

    return () => clearInterval(interval);
  }, [timeLeft, isTimerAnimated]);

 const handleAnimationState = () => {
    if (isTimerAnimated) {
      setIsTimerAnimated(false);
    } else {
      setIsTimerAnimated(true);
      setTimeLeft(totalTime);
    }
  };

  return (
    <div>
      <div className="progress-bar">
        <div
          className={`${isTimerAnimated ? "animated" : ""}`}
          style={{
            transform: `scaleX(${timeLeft ? timeLeft / totalTime : 0})`,
            animationDuration: `${totalTime}s`,
          }}
        />
      </div>
      <button onClick={handleAnimationState}>{isTimerAnimated ? "Pause the animation" : "Restart the animation"}</button>
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));
@keyframes timerAnimation {
  from {
    transform: scaleX(1);
  }
  to {
    transform: scaleX(0);
  }
}

.progress-bar {
  height: 10px;
  width: 300px;
  background-color: #fdb913;
  border-radius: 10px;
  overflow: hidden;
  margin-top: 2rem;
  margin-bottom: 3rem;
}

.progress-bar div {
  background-color: grey;
  height: 100%;
  animation-play-state: paused;
  animation-fill-mode: forwards;
  transform-origin: left center;
}

.progress-bar div.animated {
  animation: timerAnimation linear;
  animation-play-state: running;
}
<div id="root">
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js"></script>

</div>

2

Answers


  1. I have changed the timeLeft state to track milliseconds (totalTime * 1000) instead of seconds
    I have changed the interval to run every 100 milliseconds (setInterval callback) instead of every second enhances the responsiveness of your progress bar.

    const { useState, useEffect } = React;
    
    const App = () => {
      const totalTime = 10; // Total time in seconds
      const [timeLeft, setTimeLeft] = useState(totalTime * 1000);
      const [isTimerAnimated, setIsTimerAnimated] = useState(true);
      const [progress, setProgress] = useState(1);
    
      useEffect(() => {
        let interval;
        if (isTimerAnimated && timeLeft > 0) {
          interval = setInterval(() => {
            setTimeLeft(prevTimeLeft => prevTimeLeft - 100);
            setProgress(timeLeft / (totalTime * 1000));
          }, 100);
        } else if (timeLeft <= 0) {
          setIsTimerAnimated(false);
        }
        return () => clearInterval(interval);
      }, [timeLeft, isTimerAnimated]);
    
      const handleAnimationState = () => {
        if (isTimerAnimated) {
          setIsTimerAnimated(false);
        } else {
          setIsTimerAnimated(true);
          setTimeLeft(totalTime * 1000);
          setProgress(1);
        }
      };
    
      return React.createElement("div", {},
        React.createElement("div", {className: "progress-bar"},
          React.createElement("div", {
            className: "progress",
            style: {
              transform: `scaleX(${progress})`,
            }
          })
        ),
        React.createElement("button", {onClick: handleAnimationState},
          isTimerAnimated ? "Pause the animation" : "Restart the animation"
        )
      );
    };
    
    ReactDOM.render(React.createElement(App), document.getElementById("root"));
     .progress-bar {
          height: 10px;
          width: 300px;
          background-color: #fdb913;
          border-radius: 10px;
          overflow: hidden;
          margin-top: 2rem;
          margin-bottom: 3rem;
        }
    
        .progress-bar div {
          background-color: grey;
          height: 100%;
          transform-origin: left center;
        }
    <div id="root"></div>
    
      <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js"></script>
    Login or Signup to reply.
  2. You can do something like this, instead of scaling it, you can do its width, which you know will always be between 0 am 100.

    Then count your time in milliseconds. So, You can make tiny changes to the progress instead of steps. Then map your time in milliseconds to the progress

    y(progress) = (x(ms elapsed) / totalTime(s) * 1000 ) * 100
    
    const { useState, useEffect } = React;
    
    const App = () => {
      const totalTime = 4;
      const [progress, setProgress] = useState(0);
      const [elapsedMS, setElapsedMS] = useState(0);
      const [isPaused, setIsPaused] = useState(false)
      
      useEffect(() => {
        setInterval(() => {
          if(!isPaused) {
             setElapsedMS(p => p + 1)
          }
        }, 1)
      }, [])
    
    
      useEffect(() => {
         if(isPaused) setProgress((elapsedMS / (totalTime * 1000)) * 100)
      }, [elapsedMS])
    
      const handleAnimationState = () => {
        setIsPaused(p => !p)
      }
    
      
    
    return (
         <div>
            <div className="progress-bar">
               <div
                 className={`${isPaused ? "animated" : ""}`}
                 style={{
                    width: `${100 - progress}%`,
                 }}
               />
            </div>
            <button onClick={handleAnimationState}>{isPaused ? "Pause the animation" : "Restart the animation"}</button>
        </div>
      );
    
    };
    
    ReactDOM.render(<App />, document.getElementById("root"));
    @keyframes timerAnimation {
      from {
        transform: scaleX(1);
      }
      to {
        transform: scaleX(0);
      }
    }
    
    .progress-bar {
      height: 10px;
      width: 300px;
      background-color: #fdb913;
      border-radius: 10px;
      overflow: hidden;
      margin-top: 2rem;
      margin-bottom: 3rem;
    }
    
    .progress-bar div {
      background-color: grey;
      height: 100%;
      animation-play-state: paused;
      animation-fill-mode: forwards;
      transform-origin: left center;
    }
    
    .progress-bar div.animated {
      animation: timerAnimation linear;
      animation-play-state: running;
    }
    <div id="root">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js"></script>
    
    </div>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search