I was creating a progress bar component with Next.js and tailwind.css and using JavaScript setInterval()
function to animate it. Here is the code below:
import React, { useState } from "react";
const ProgressBar = () => {
// starting at 1% of the width of the progress bar
let [progress, setProgress] = useState(1);
function handleClick() {
let i = setInterval(() => {
setProgress(progress++);
},100)
if (progress == 100) {
clearInterval(i);
}
}
return (
<>
<div className={`bg-gray-300 w-full`}>
<div className={ `h-[30px] w-[${progress}%] bg-green-500`}></div>
</div>
<button onClick={handleClick}>Click me</button>
</>
);
};
export default ProgressBar;
Now the problem is that when I run the development server to test it. It appears as if the progress is completed even before I click the button. It shows the whole width of the progress bar green instead of 1% of its width before clicking the button and sometimes I get some reference errors when I try to use methods such as global variables instead of hooks and even DOM methods such as document.getElementsByClassName().style.width;
and so on please help me and you can copy it to your workspace and run the development server to see the result. Thank you.
I tried manipulating the DOM using methods like document.getElementById()
or document.getElementsByClassName()
and global variables and function-scoped variables and react hooks like useState()
but it gives me reference error to tell me that the variable of the hook that holds the initial value is not defined or global variables and function-scoped variables that I declared at the top of my component to manipulate later are not defined or it shows the progress bar completed even before clicking the button as if the setInterval
has no effect
Firstly, try and avoid direct access to the DOM, it's very rare you need to do this, and if you do you should use refs
to do it.
There are a few issues with your progress bar,
setProgress(progress++);
<-- progress here is scoped to the function, so your constantly saying setProgress(1++)
, IOW: it can never get past 2. The solution here to avoid scope is just use the callback version setProgress(progress => progress + 1);
React component's are designed to be unmounted, your current implementation doesn't take that into account, IOW: if you start an interval it will continue to 100 even when the component is not visible. The solution to this is to use useEffect
and React.useRef
, the useEffect
hook can be used to do something before the component is unmounted. The ref is used to keep track of the interval ID. (note: refs can store more than just DOM references).
Below is an example, I'v not used tailwind here, but you should be able to replace with it pretty easy. In the example I've also used a checkbox to mount / unmount for testing, if you un-comment what's in the useEffect
you will see in your browsers console that the setInterval
will continue to run even when um-mounted (before 100%), not good.
ps. Just click the progress bar to make it start, there is also a check to make if so you can start it again until it's complete.
const root = ReactDOM.createRoot(document.getElementById('root'));
const {useState, useEffect, useRef} = React;
const ProgressBar = () => {
const tm = React.useRef(null);
const [percent, setPercent] = useState(0);
if (percent >= 100) {
clearInterval(tm.current);
tm.current = null;
}
useEffect(() => {
return () => {
//take the below code out,
//start a progress then umount
if (tm.current !== null)
clearInterval(tm.current);
}
},[]);
return <div className="progress-bar" onClick={() => {
//remove the below line if you want to restart before 100%
if (tm.current !== null) return;
clearInterval(tm.current);
setPercent(0);
tm.current = setInterval(() => {
console.log(new Date().getTime());
setPercent(p => p +1);
}, 20);
}}>
<div className="progress-bar-inner" style={{width: `${percent}%`}}></div>
<div className="progress-bar-text">{percent}%</div>
</div>
}
function UnmountTest() {
const [vis, setVis] = useState(true);
return <React.Fragment>
Show Progress Bar <input
type="checkbox" checked={vis}
onChange={e => setVis(e.target.checked)}
/>
{vis && <ProgressBar/>}
</React.Fragment>;
}
root.render(<UnmountTest/>);
.progress-bar {
background-color: silver;
height: 20px;
border: 1px solid black;
position: relative;
overflow: none;
user-select: none;
}
.progress-bar-inner {
position: absolute;
width: 40%;
height: 100%;
background-color: lightblue;
}
.progress-bar-text {
position: absolute;
width: 100%;
text-align: center;
}
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div id="root"></div>