I'm making a timer progress bar animation in react. I'm doing this using requestAnimationFrame()
. The issue I have is that I cannot stop that animation with cancelAnimationFrame()
despite setting an id to the animation and then referring to that id while cancelling (handleStop
func). Why?
Edit: Hmm, maybe it stops requestAnimationFrame()
but it won't stop transform animation that was already set?
const { useState, useEffect } = React;
const App = () => {
const totalTime = 10000;
const [timeLeft, setTimeLeft] = useState(null);
const [timerId, setTimerId] = useState(null);
useEffect(() => {
setTimeLeft(totalTime);
const start = performance.now();
const animate = (time) => {
const elapsedTime = time - start;
setTimeLeft((prevTimeLeft) => {
if (prevTimeLeft !== null && prevTimeLeft > 0) {
const remainingTime = Math.max(totalTime - elapsedTime, 0);
requestAnimationFrame(animate);
return remainingTime;
}
return 0;
});
};
const id = requestAnimationFrame(animate);
setTimerId(id);
return () => {
cancelAnimationFrame(id);
};
}, []);
const handleStop = () => {
if (timerId) {
cancelAnimationFrame(timerId);
setTimerId(null);
}
};
return (
<div>
<div className="progress-bar">
<div
style={{
transform: `scaleX(${timeLeft ? timeLeft / totalTime : 0})`,
}}
/>
</div>
<button onClick={handleStop}>Pause the animation</button>
</div>
);
};
ReactDOM.render(<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">
<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>
Maybe you can try these steps.
setState()
callback function.useRef()
instead of useState()
to manage timer id.I wrote the code with those steps and it worked well in this code sandbox.
const timerRef = useRef(null);
useEffect(() => {
setTimeLeft(totalTime);
const start = performance.now();
const animate = (time) => {
const elapsedTime = time - start;
setTimeLeft((prevTimeLeft) => {
if (prevTimeLeft !== null && prevTimeLeft > 0) {
const remainingTime = Math.max(totalTime - elapsedTime, 0);
return remainingTime;
}
return 0;
});
timerRef.current = requestAnimationFrame(animate);
};
timerRef.current = requestAnimationFrame(animate);
return () => {
if (timerRef.current !== null) {
cancelAnimationFrame(timerRef.current);
timerRef.current = null;
}
};
}, []);