Showing Topbar Progress Indicator in React
I use circular loading indicators in my React applications before data is returned from the server:
Which is a simple SVG file loaded as a React component:
// other imports
import { ReactComponent as Circular } from "./assets/CircularLoading.svg";// highlight-line
function App() {
const { data, isLoading } = useQuery<User[]>("users", () => getUsers(), {})
return (
<div>
{isLoading && <Circular className="circular" /> // highlight-line}
{data && (
<table>
<thead>
<tr>
<td>ID</td>
<td>Name</td>
<td>Email</td>
</tr>
</thead>
<tbody>
{data.map(user => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.email}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)
}
export default App
In one of our projects I was asked to add a topbar loading indicator:
Since it’s an SVG file, it’s pretty easy to do. However, it should be based on the elapsed time. As opposed to the first one, which spins as long as isLoading
is set to true
, for this one we will have to calculate the actual elapsed time and convert it to percentage, then use it as width
for the loading indicator.
function App() {
const [progress, setProgress] = useState(0) // highlight-line
const { data, isLoading } = useQuery<User[]>(
"users",
() =>
// highlight-start
getUsers(progressEvent => {
let percentCompleted = Math.floor(
(progressEvent.loaded / progressEvent.total) * 100
)
setProgress(percentCompleted)
}),
// highlight-end
{}
)
return (
<div>
{isLoading && <TopbarLoading value={progress} /> // highlight-line}
{isLoading && <Circular className="circular" />}
{data && (
<table>
<thead>
<tr>
<td>ID</td>
<td>Name</td>
<td>Email</td>
</tr>
</thead>
<tbody>
{data.map(user => (
<tr key={user.id}>
<td>{user.id}</td>
<td>{user.name}</td>
<td>{user.email}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)
}
// highlight-start
const TopbarLoading = styled(Linear)<{ value?: number }>`
#progress rect {
transition: width 0.5s;
width: ${props => (props.value && `${props.value}px`) || "unset"};
}
`
// highlight-end
export default App
You can see from the code above that we changed the service method slightly. It now accepts a callback that is used to update the progress state. The implementation below utilizes the integrated functionality of axios:
export const getUsers = (
onDownloadProgress?: (progressEvent: {
loaded: number;
total: number;
}) => void
) =>
client
.get("/api/users", {
onDownloadProgress,
})
.then((res) => (res as AxiosResponse).data);
If you just want to display a loading bar that goes backwards and forwards, you do not need to pass the onDownloadProgress
event. All you need to do is use a timer to generate random values while the data is being loaded:
function App() {
let timer: number; // highlight-line
const [progress, setProgress] = useState(0);
const { data, isLoading } = useQuery<User[]>("users", () => getUsers(), {
onSuccess: () => clearInterval(timer), // highlight-line
});
// highlight-start
useEffect(() => {
timer = setInterval(() => {
setProgress((prev) => {
if (prev === 100) {
return 0;
}
return Math.min((prev + Math.random() * 20) % 100, 100);
});
}, 500);
return () => {
clearInterval(timer);
};
}, []);
// highlight-end
return (
<div>
{isLoading && <TopbarLoading value={progress} />}
{isLoading && <Circular className="circular" />}
{data && (
<table>
<thead>
<tr>
<td>ID</td>
<td>Name</td>
<td>Email</td>
</tr>
</thead>
<tbody>
{data.map((user) => (
<tr key={user.id}>
<td>{user.id}</td>
<td>{user.name}</td>
<td>{user.email}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
}
const TopbarLoading = styled(Linear)<{ value?: number }>`
#progress rect {
transition: width 0.5s;
width: ${(props) => (props.value && `${props.value}px`) || "unset"};
}
`;
export default App;