The rewritten title is: Calling React Promise.all().then() before each setState from promise.then() is rendered
P粉162773626
2023-08-20 11:02:53
<p>I'm trying to make 200 calls to a remote resource to display in my table, while displaying a progress bar to show the number of calls remaining. </p>
<p>Use this example to demonstrate how to use <code>Fetch()</code> and <code>Promise.all()</code> to call <code>setState()</ code> to update new data. </p>
<p>My problem is with each promise's <code>.then()</code>, which calculates some logic and then calls <code>setState()</code> to update the data. </p>
<p>My progress bar uses <code>Object.keys(data).length</code> to show progress. </p>
<p>After <code>Promise.all()</code> triggers the "Complete" state, removing the progress bar, the promises themselves are still calling their <code>then()</code> , which causes the progress bar to be hidden before all resolved promises are displayed. </p>
<p>How to deal with this problem correctly? </p>
<hr />
<p>Demo, use <code>setTimeout()</code> to simulate expensive logic. </p>
<p>The problem is that <code>Promise.all.then: 20</code> should be after <code>Render 20</code>. </p>
<pre class="brush:none;toolbar:false;">Render 0
...
Render 12
Promise.all.then: 20 # I need this to be recorded after each Render
Render 13
...
Render 19
Render 20
</pre>
<p>To make the demo show the problem, the progress bar was removed (turned red) before it was completely filled.</p>
<p><br /></p>
<pre class="brush:js;toolbar:false;">const { useState } = React;
const Example = () => {
const [done, setDone] = useState(false);
const [data, setData] = useState({});
const demoData = Array.from(Array(20).keys());
const demoResolver = (x) => new Promise(res => setTimeout(() => res(x), Math.random() * 1250))
const loadData = () => {
const promises = demoData.map(c => demoResolver(c));
promises.forEach(promise => {
promise
.then(r => {
setTimeout(() => {
setData(p => ({ ...p, [r]: r }));
}, 500);
})
});
Promise.all(promises)
.then(r => {
console.log('Promise.all.then: ', r.length)
setDone(true);
})
}
console.log('Render', Object.keys(data).length);
const progressBarIsShownDebugColor = (done)
? 'is-danger'
: 'is-info';
return (
<section className='section'>
<h1 className='title is-3'>{'Example'}</h1>
<progress
max={demoData.length}
value={Object.keys(data).length}
className={'progress my-3 ' progressBarIsShownDebugColor}
/>
<button onClick={loadData}>Start</button>
</section>
)
}
ReactDOM.render(<Example />, document.getElementById("react"));</pre>
<pre class="brush:css;toolbar:false;">.as-console-wrapper { max-height: 50px !important; }</pre>
<pre class="brush:html;toolbar:false;"><script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<div id="react"></div></pre>
<p><br /></p>
The problem shown in the code above is that after getting the data, there is an additional 500ms async delay before setting the state. In the actual code, it sounds like there is additional processing (probably synchronous) that causes
setData
to be called after.all
.The best practice is to have
done
as a computed property rather than a separate state, because at that point you don't need to rely on the state to set up contention, andObject.keys( data).length
is cheap enough that it doesn't hurt performance (and you use it in other areas, you can cache it into a variable if it becomes a problem).