3

Imagine I got an array of promise (12 promise total), I want to render the progress of the promise has resolved on page like: 1/12, 2/12, 3/12 something like that. So I got the idea of how to do it from this answer here: link

I successfully calculated the progressNum or the percentage, and able to console.log them.

The problem is when I try to use setState to set the progressNum, its only show 12/12 when all the promise have resolved. Or render some random num like 4/12 and then 12/12 after, but I want to render something like go from 1/12, 2/12, 3/13 ... 12/12.

Im able to console.log the progress correctly

not render correctly

And I know setState is async, so I try to use react ref to manipulate the element. But didn't get any luck too.

My code so far:

class App extends Component {
  state = {
   progress: 0,
  };

  handleResize = async () => {
   ...

   // imgFiles is an array 12 File object

   this.allProgress(imgFiles.map(this.resizeImg));

   ...
  };


   allProgress(promArray) {
    let progress = 0;
    promArray.forEach((p) => {
      p.then(()=> {    
        progress += 1;
        console.log(`${progress}/12`);
        this.setState({ progress });
      });
    });
    return Promise.all(promArray);
  }

 // I use Jimp package to resize the img, and then return promise

   resizeImg = imgFile => new Promise((resolve, reject) => {
   const reader = new FileReader();
   reader.onloadend = () => {
     Jimp.read(reader.result)
         .then(async (jimpImg) => {
         const normalImg = await this.resizeMain(jimpImg, true, 
         imgFile.name);
         const largeImg = await this.resizeMain(jimpImg, false, 
         imgFile.name);
        resolve([normalImg, largeImg]);
      })
      .catch(reject);
     };
     reader.readAsArrayBuffer(imgFile);
   });

 render() {
  return (
    <div>
     <p>{this.state.progress}</p>
     <button onClick={this.handleResize} >Resize</button>
    </div> )
 }

I also try ref

class App extends Component {
 state = {
  progress: 0,
 };

 indicator = React.createRef();

 changeProgress = (num) => {
    this.indicator.current.innerText = `${num}/12`;
  };

 ...

 allProgress(promArray) {
  let progress = 0;
  promArray.forEach((p) => {
   p.then(()=> {    
    progress += 1;
    console.log(`${progress}/12`);

    // the only logic that I changed:        

    this.changeProgress(progress);
   });
  });
   return Promise.all(promArray);
 } 

 ...

 render() {
  return (
   <div>
    <p ref={this.indicator} />
    <button onClick={this.handleResize} >Resize</button>
   </div> )
 }
}
plat123456789
  • 385
  • 3
  • 12
  • Can you consistently reproduce the problem, or, ideally, come up with a snippet that reproduces the problem? (Similar logic seems to be working fine here: https://jsfiddle.net/mgduf70k/ ) Very wild guess, is CPU usage consistent, or might there sometimes be too many resources being used elsewhere for the browser to repaint itself? – CertainPerformance Nov 09 '18 at 09:40
  • Update: I cnsole.log the state, the state has changed accordingly. but p tag element don't render – plat123456789 Nov 09 '18 at 10:49
  • Can you come up with a [MCVE] that reproduces the problem? As you can see from the comments below both answers, it's not clear what the actual issue is coming from – CertainPerformance Nov 09 '18 at 11:00

2 Answers2

2

You can use the callback version of setState when you want to update the progress to make sure you don't try to update with an old value.

Example

class App extends React.Component {
  state = {
    done: 0,
    total: 12,
    data: []
  };

  componentDidMount() {
    Promise.all(
      Array.from({ length: this.state.total }, () => {
        return new Promise(resolve => {
          setTimeout(() => {
            this.setState(
              prevState => ({ done: prevState.done + 1 }),
              () => resolve(Math.random())
            );
          }, Math.random() * 3000);
        });
      })
    ).then(data => {
      this.setState({ data });
    });
  }

  render() {
    const { done, total, data } = this.state;
    return (
      <div>
        <div>
          {done} / {total} done
        </div>
        <div>{data.join(", ")}</div>
      </div>
    );
  }
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

<div id="root"><div>
Tholle
  • 108,070
  • 19
  • 198
  • 189
  • 1
    Can you explain why the `progress++` method OP is using doesn't work (results in the batch behavior in the videos), or how his code can be updating "with an old value"? Might be a React thing I'm not familiar with but as the `console.log` video shows, the `setState({ progress })` *looks* to be occuring with the right `progress` number, and it seems to be working fine here https://jsfiddle.net/mgduf70k/ – CertainPerformance Nov 09 '18 at 09:24
  • @CertainPerformance Hm, yes, you're right. My explanation is misleading, since it should work. It's generally good practice to update state that is derived from data already in state with the callback version of `setState`, since mutation can introduce subtle bugs. I'm not sure anymore what might be happening in OP's question, though. – Tholle Nov 09 '18 at 09:28
2

The problem is that you are using Promise.all which will be only only called when all the promises are resolved. So all your then methods will be called together. So multiple setState calls inside callbacks will be batched together, that's why you are not seeing the whole count. Dont use Promise.all to solve the problem and let the promises resolve in their natural order.

class App extends React.Component{
  constructor(props){
    super(props)
    this.state = {progress: 0, total: 12}
  }

  allProgress(promArray) {
    let progress = 0;
    //dont use Promise.all
    promArray.forEach((p) => {
      p.then(()=> {    
        progress += 1;
        console.log(`${progress}/12`);
        this.setState({ progress });
      });
    });
  }

  componentDidMount(){
    const p = []
    for(let i =0;i<12;i++){
      p.push(new Promise((resolve, reject)=>{setTimeout(resolve, Math.random() * 5000)}))
    }
    this.allProgress(p)
  }
  render(){
    return (
      <div>{this.state.progress} / {this.state.total}</div>
    )
  }
}

ReactDOM.render(<App />, document.getElementById("app"))
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
Anurag Awasthi
  • 6,115
  • 2
  • 18
  • 32
  • But the `setState` inside `allProgress` isn't linked to the `Promise.all`? (The `.then`s that then call `setState` occur after *individual* promises resolve, or so it looks from OP's code, so what's causing the batch behavior?) OP's resulting `Promise.all` isn't being used anywhere. Genuine question, I'm not so familiar with React – CertainPerformance Nov 09 '18 at 09:05
  • @CertainPerformance yeah you are right, `Promise.all` is not being used. My reasoning is wrong. – Anurag Awasthi Nov 09 '18 at 09:17
  • I edited the title from promise.all to array to promise – plat123456789 Nov 09 '18 at 09:20
  • @plat123456789 does removing `Promise.all` makes any difference? – Anurag Awasthi Nov 09 '18 at 09:22