React/TypeScript图片预加载遇更新深度超限,如何实现进度条?
解决React图片预加载的无限更新错误并实现进度条
我想在网站加载前实现一个进度条,通过预加载图片及其他资源将加载进度转换为百分比。用了以下React/TypeScript脚本做图片预加载,但遇到了「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.
原代码如下:
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 />);
错误原因分析
componentDidUpdate会在每次state更新后触发。当图片加载完成,totalImagesLoaded等于totalImagesToLoad时,调用setLoading(false)会触发state更新,再次进入componentDidUpdate。此时判断条件依然成立,又会重复调用setLoading(false),形成无限循环,触发报错。
重构方案与实现
1. 修复无限循环
在componentDidUpdate中增加状态判断,仅当当前处于加载状态时才切换为已加载状态:
componentDidUpdate(prevProps, prevState) { if (this.state.loading && this.state.totalImagesLoaded >= this.totalImagesToLoad) { this.setLoading(false); } }
2. 实现进度百分比计算
通过(已加载数量 / 总数量) * 100计算进度,同时处理总数量为0的边界情况避免出现NaN:
const progress = this.totalImagesToLoad > 0 ? Math.round((totalImagesLoaded / this.totalImagesToLoad) * 100) : 0;
3. 优化图片预加载逻辑
用原生Image对象代替XHR,更贴合图片预加载场景,代码更简洁:
cacheImagesProgress = (srcArray: Array<string>) => { this.totalImagesToLoad = srcArray.length; srcArray.forEach(src => { const img = new Image(); img.onload = this.totalLoadingProgress; // 处理加载失败,避免进度卡住 img.onerror = this.totalLoadingProgress; img.src = src; }); }
4. 优化state更新方式
使用函数式更新setState,避免依赖当前state值导致的异步更新问题:
totalLoadingProgress = () => { this.setState(prevState => ({ totalImagesLoaded: prevState.totalImagesLoaded + 1 })); }
完整重构代码
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 = () => { // 函数式更新确保拿到最新的state值 this.setState(prevState => ({ totalImagesLoaded: prevState.totalImagesLoaded + 1 })); }; cacheImagesProgress = (srcArray: Array<string>) => { this.totalImagesToLoad = srcArray.length; srcArray.forEach(src => { const img = new Image(); img.onload = this.totalLoadingProgress; // 处理加载失败,避免进度卡住 img.onerror = this.totalLoadingProgress; img.src = src; }); }; componentDidMount() { this.setLoading(true); const folder = "https://picsum.photos/"; const images = [ folder + "200/300", folder + "400/500", folder + "800/500" ]; this.cacheImagesProgress(images); } componentDidUpdate(prevProps, prevState) { // 仅当当前处于加载状态且所有图片加载完成时,才切换状态 if (this.state.loading && this.state.totalImagesLoaded >= this.totalImagesToLoad) { this.setLoading(false); } } render() { const { loading, totalImagesLoaded } = this.state; const progress = this.totalImagesToLoad > 0 ? Math.round((totalImagesLoaded / this.totalImagesToLoad) * 100) : 0; return ( <main className="app"> {loading ? ( <div className="loading-container"> <div className="progress-bar"> <div className="progress-fill" style={{ width: `${progress}%` }} /> </div> <p>加载中:{progress}%</p> </div> ) : ( <div>加载完成!</div> )} </main> ); } } export default App; const container = document.getElementById("root"); const root = ReactDOM.createRoot(container); root.render(<App />);
可选样式补充
添加简单CSS让进度条更直观:
.loading-container { width: 80%; margin: 20px auto; text-align: center; } .progress-bar { width: 100%; height: 20px; border: 1px solid #ccc; border-radius: 10px; overflow: hidden; margin-bottom: 10px; } .progress-fill { height: 100%; background-color: #4CAF50; transition: width 0.3s ease; }
内容的提问来源于stack exchange,提问作者Erwin
相关产品推荐
相关产品推荐

