在Typescript / React中预加载图像:触发了最大更新深度限制错误。

huangapple go评论70阅读模式
英文:

Preloading images in Typescript / React: Maximum update depth exceeded

问题

我尝试在加载网站之前添加一个进度条。我尝试预加载图像(和其他项目)并将其转换为加载完成的百分比。

我有以下脚本来在React网页中预加载图像:

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/'; // 设置
        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');

// 创建一个根元素。
const root = ReactDOM.createRoot(container);

// 初始渲染
root.render(<App />);

查看此Sandbox:https://codesandbox.io/s/maximum-update-depth-exceeded-preload-images-forked-6jdhvx

我收到错误消息 "Maximum update depth exceeded"。

这可能是因为一个组件在componentWillUpdate或componentDidUpdate内部重复调用setState。React 限制嵌套更新的数量以防止无限循环。

如何重构代码以便根据加载的图像数量显示进度条?(已加载5张图像中的4张= 80%进度条)

英文:

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 &quot;react&quot;;
import ReactDOM from &quot;react-dom&quot;;
interface AppInterface {
loading: boolean,
totalImagesLoaded: number
}
class App extends React.Component&lt;{}, AppInterface&gt; {
private totalImagesToLoad = 0;
constructor(props: {}) {
super(props);
this.state = {
loading: false,
totalImagesLoaded: 0
}
}
setLoading = (loadingstate: boolean) =&gt; {
this.setState({
loading: loadingstate
})
}
totalLoadingProgress = () =&gt; {
this.setState({
totalImagesLoaded: this.state.totalImagesLoaded + 1
})
}
cacheImagesprogress = (srcArray: Array&lt;string&gt;) =&gt; {
const self = this;
srcArray.map((src:string, index:number) =&gt; {
const xhr = new XMLHttpRequest();
xhr.open(&quot;GET&quot;, src, true);
xhr.onload = function (e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
self.totalLoadingProgress();
}
}
};
xhr.send();
});
}
componentDidMount() {
this.setLoading(true);
const folder = &#39;https://picsum.photos/&#39;; // settings
const images = [
folder + &#39;200/300&#39;,
folder + &#39;400/500&#39;,
folder + &#39;800/500&#39;,
]
this.totalImagesToLoad = images.length;
this.cacheImagesprogress(images);
}
componentDidUpdate() {
if(this.state.totalImagesLoaded &gt;= this.totalImagesToLoad) {
this.setLoading(false);
}
}
render() {
return (
&lt;main className=&quot;app&quot;&gt;
{this.state.loading ? (
&lt;&gt;
&lt;div&gt;Loading..&lt;/div&gt;
&lt;/&gt;
) : (
&lt;&gt;
Done loading!
&lt;/&gt;
)}
&lt;/main&gt;
)
}
}
export default App;
const container = document.getElementById(&#39;root&#39;);
// Create a root.
const root = ReactDOM.createRoot(container);
// Initial render
root.render(&lt;App /&gt;);

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)

答案1

得分: 0

这将是因为在 componentDidUpdate 中调用了 this.setState,这会导致重新渲染,然后会再次触发 componentDidUpdate,进而导致再次调用 this.setState……以此类推。即使值与先前的相同,React 也会重新渲染。值得注意的是,这仅在您使用的旧式类组件中发生。在 hooks 模式中使用 useState 不会在相同情况下重新渲染。

componentDidUpdate 更新为检查加载标志:

componentDidUpdate() {
    if (loading !== false && this.state.totalImagesLoaded >= this.totalImagesToLoad) {
        this.setLoading(false);
    }
}

使用 hooks 会更清晰许多(类已经是过时的 API 了。虽然这很糟糕,但他们尚未将主要站点迁移到 beta 文档 中。旧文档大量使用了已弃用的类 API)。同时使用基于 Promise 的 fetch 会更好:

import React, { useState, useEffect, FC } from "react";

const folder = 'https://picsum.photos/'; // 设置
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');

// 创建根节点。
const root = ReactDOM.createRoot(container);

// 初始渲染
root.render(<App />);

对于进度条,我建议不要重复造轮子,可以引入类似 Chakramaterial-ui 这样的设计系统,它们已经准备好可以直接使用的进度条组件。

英文:

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 &amp;&amp; this.state.totalImagesLoaded &gt;= 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 &quot;react&quot;;
const folder = &#39;https://picsum.photos/&#39;; // settings
const images = [
folder + &#39;200/300&#39;,
folder + &#39;400/500&#39;,
folder + &#39;800/500&#39;,
]
const App: FC = () =&gt; {
const [totalImagesLoaded, setTotalImagesLoaded] = useState&lt;number&gt;(0)
const [loading, setLoading] = useState&lt;boolean&gt;(true)
useEffect(() =&gt; {
Promise.all(images.map((image) =&gt;
fetch(image).then((res) =&gt; {
if (!response.ok) return
setTotalImagesLoaded((prev) =&gt; prev + 1)
})
)).finally(() =&gt; setLoading(false))
}, [])
return (
&lt;main className=&quot;app&quot;&gt;
{loading ? (
&lt;&gt;
&lt;div&gt;Total: {Math.round((totalImagesLoaded / images.length))*100}%&lt;/div&gt;
&lt;/&gt;
) : (
&lt;&gt;Done loading!&lt;/&gt;
)}
&lt;/main&gt;
)
}
export default App;
const container = document.getElementById(&#39;root&#39;);
// Create a root.
const root = ReactDOM.createRoot(container);
// Initial render
root.render(&lt;App /&gt;);

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.

huangapple
  • 本文由 发表于 2023年2月24日 02:14:46
  • 转载请务必保留本文链接:https://go.coder-hub.com/75548774.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定