I’m trying to call 200 remote resources to show in my table, with a progress bar showing the amount of calls remaining.
Using this example on how to use Fetch()
with Promise.all()
to call setState()
with the new data.
My problem lies in the .then()
for each promise, which computes some logic, and then calls setState()
to update the data.
My progress bar uses Object.keys(data).length
to show the progress.
After the Promise.all()
triggers the ‘done’ state, removing the progess bar, the promises themself are still calling there then()
which causes the progress bar to be hidden before it shows the full amount of promises resolved.
What is the correct way of dealing with this?
Demo, which uses setTimeout()
to mimic the expensive logic.
Issue is that the Promise.all.then: 20
should become after Render 20
.
Render 0
...
Render 12
Promise.all.then: 20 # I need this to log after each Render
Render 13
...
Render 19
Render 20
To make the demo show the issue the progress bar is removed (turned red) before it’s completely full.
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"));
.as-console-wrapper { max-height: 50px !important; }
<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/[email protected]/css/bulma.min.css">
<div id="react"></div>
2
Answers
Your issue is that the promises are already resolved when their callbacks are called, meaning that
Promise.all
will always be resolved before the execution of the callback of the last setTimeout.If you want to keep the delay, I think you should put the
setTimeout
inside thedemoResolver
The issue shown in the code above is that you have an additional 500ms async delay after getting the data before setting the state. In the real code, it sounds similar that there is extra processing (probably sync) causing the
setData
s to be called after the.all
.The best thing to do is to make
done
be a computed property instead of a separate state, since at that point you don’t need to rely on state setting races andObject.keys(data).length
is cheap enough where it won’t decrease performance (plus you’re using it in other areas, if it became a concern you could cache it in a variable).