I try to have a progress bar before loading a website. I try to preload images (and other items) and convert it in a percentage of loading complete.
I have the following script to preload images in a React webpage:
import React from "react";
import ReactDOM from "react-dom";
interface AppInterface {
loading: boolean,
totalImagesLoaded: number
}
class App extends React.Component<{}, AppInterface> {
private totalImagesToLoad = 0;
constructor(props: {}) {
super(props);
this.state = {
loading: false,
totalImagesLoaded: 0
}
}
setLoading = (loadingstate: boolean) => {
this.setState({
loading: loadingstate
})
}
totalLoadingProgress = () => {
this.setState({
totalImagesLoaded: this.state.totalImagesLoaded + 1
})
}
cacheImagesprogress = (srcArray: Array<string>) => {
const self = this;
srcArray.map((src:string, index:number) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", src, true);
xhr.onload = function (e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
self.totalLoadingProgress();
}
}
};
xhr.send();
});
}
componentDidMount() {
this.setLoading(true);
const folder = 'https://picsum.photos/'; // settings
const images = [
folder + '200/300',
folder + '400/500',
folder + '800/500',
]
this.totalImagesToLoad = images.length;
this.cacheImagesprogress(images);
}
componentDidUpdate() {
if(this.state.totalImagesLoaded >= this.totalImagesToLoad) {
this.setLoading(false);
}
}
render() {
return (
<main className="app">
{this.state.loading ? (
<>
<div>Loading..</div>
</>
) : (
<>
Done loading!
</>
)}
</main>
)
}
}
export default App;
const container = document.getElementById('root');
// Create a root.
const root = ReactDOM.createRoot(container);
// Initial render
root.render(<App />);
See this Sandbox: https://codesandbox.io/s/maximum-update-depth-exceeded-preload-images-forked-6jdhvx
I get the error "Maximum update depth exceeded"
This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
How can I refactor this so I can show a progress bar based on the number of images loaded? (4 of 5 images loaded = 80% progress bar)
This'll be because in componentDidUpdate
you call this.setState
which will cause a rerender, this then causes componentDidUpdate
to run, which causes the this.setState
to run again...and so on. React will rerender even if the value is the same as the previous one. It's notable that this only happens in the old style class components you are using. useState
in the hooks pattern does not rerender if it's the same.
Update componentDidUpdate
to check the loading flag:
componentDidUpdate() {
if(loading !== false && this.state.totalImagesLoaded >= this.totalImagesToLoad) {
this.setLoading(false);
}
}
Things would be a lot cleaner with hooks (classes are an unused API nowadays. It's terrible but they have not moved the main site to the beta docs yet. The old docs make a lot of use of the dead classes API). and also using fetch
(which is promise based) though:
import React, { useState, useEffect, FC } from "react";
const folder = 'https://picsum.photos/'; // settings
const images = [
folder + '200/300',
folder + '400/500',
folder + '800/500',
]
const App: FC = () => {
const [totalImagesLoaded, setTotalImagesLoaded] = useState<number>(0)
const [loading, setLoading] = useState<boolean>(true)
useEffect(() => {
Promise.all(images.map((image) =>
fetch(image).then((res) => {
if (!response.ok) return
setTotalImagesLoaded((prev) => prev + 1)
})
)).finally(() => setLoading(false))
}, [])
return (
<main className="app">
{loading ? (
<>
<div>Total: {Math.round((totalImagesLoaded / images.length))*100}%</div>
</>
) : (
<>Done loading!</>
)}
</main>
)
}
export default App;
const container = document.getElementById('root');
// Create a root.
const root = ReactDOM.createRoot(container);
// Initial render
root.render(<App />);
For the progress bar, I would recommend not reinventing the wheel and introducing a design system like Chakra
or material-ui
, which have it readily available to pipe in.