skip to Main Content

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


  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)
    })
    
    Login or Signup to reply.
  2. 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/[email protected]/css/bulma.min.css">
    <div id="react"></div>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search