0

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/bulma@0.9.4/css/bulma.min.css">
<div id="react"></div>
0stone0
  • 34,288
  • 4
  • 39
  • 64
  • I'm a bit confused-- why are you running all the promises twice? – Matt Morgan Jun 27 '23 at 15:15
  • 1
    @MattMorgan Promises only run once, ever. If you call `.then` again *after it's already resolved*, or in this case `.all` on an array of Promises, it resolves immediately. In this specifc case, since it's not awaited, an additional `.then` is added to each Promise, which is queued up for once it resolves. – Samathingamajig Jun 27 '23 at 15:19
  • What is the point of the `setTimeout` with the 500ms delay?! – Bergi Jun 27 '23 at 18:52

2 Answers2

1

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 setDatas 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 and Object.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).

const [data, setData] = useState({});
const done = Object.keys(data).length === 20; // 200 in the real code

const { useState } = React;

const Example = () => {

    const [data, setData] = useState({});
    const done = Object.keys(data).length === 20; // 200 in the real code
      
    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);
                })
        });
    }
    
    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/bulma@0.9.4/css/bulma.min.css">
<div id="react"></div>
Samathingamajig
  • 11,839
  • 3
  • 12
  • 34
  • Thanks, this gave me a push in the right direction. I'm now using an `useEffect` which checks if `done` needs to be set and then do the logic that first was in the `Promise.all().then`, not sure why I didn't think of that earlier. I'll wait until tomorrow should somebody else come along with a better answer, otherwise I'll accept yours. – 0stone0 Jun 27 '23 at 16:13
-1

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 the demoResolver

const demoResolver = (x) => new Promise(res => {
  setTimeout(() => { // I guess this is to mock the delay of an api call
    setTimeout(() => {
      setData(p => ({ ...p, [x]: x })); 
      res(x);
    }, 500)
  }, Math.random() * 1250)
})
Florent M.
  • 1,107
  • 1
  • 6
  • 14
  • The 'delay' is just to mimic my ApiCall + logic which takes some time, can't change the `demoResolver` in this demo. – 0stone0 Jun 27 '23 at 15:24
  • I can only help you with the code you provided, now you know what the problem is so it is up to you to adapt the solution to a real use case :) But if you have any question you can ask – Florent M. Jun 27 '23 at 15:29