skip to Main Content

I try to report to the user the progress of the promise execution. The problem I’m encountering is that React seems to be merging my setState calls into a single render.

When I click start, it flashes 0 / 5 and then stays 1 / 5, and other times it comes up higher than 1. There is also a problem with execution stopping when the button is pressed, because setState doesn’t have time to update total and thus stops the code before the button is pressed (the commented line). The code below is my attempt.

import React, { useState } from "react";

export default function ({}) {
  const [state, setState] = useState({ progress: 0, total: 0 });

  const wait = (duration) => () => new Promise((resolve) => setTimeout(resolve, duration));

  const startWork = async () => {
    const tasks = [wait(100), wait(200), wait(300), wait(400), wait(500)];

    setState({ ...state, progress: 0, total: tasks.length });

    for (const task of tasks) {
      // stop if cancelled
      //   if (state.total === 0) return;

      const data = await task();

      setState({ ...state, progress: state.progress + 1 });
    }
  };

  const cancelWork = () => {
    setState({ ...state, total: 0 });
  };

  return (
    <div>
      <p>
        {state.progress} / {state.total}
      </p>
      <button onClick={startWork}>Start</button>
      <button onClick={cancelWork}>Cancel</button>
    </div>
  );
}

2

Answers


  1. setState updates the state on the next render. When you do

    setState({ ...state, progress: 0, total: tasks.length });
    for (const task of tasks) {
      setState({ ...state, progress: state.progress + 1 });
    }
    

    state.progress will be the initial value every iteration of the loop, because it hasn’t re-rendered yet, so each time you will be setting it to 0 + 1 (assuming state.progress was initially 0).

    What you probably want to do is:

    let progress = state.progress || 0;
    setState({ ...state, progress, total: tasks.length });
    for (const task of tasks) {
        progress += 1;
        setState({ ...state, progress });
    }
    

    so that each time you call setState, you are working with the correct value of progress.

    Login or Signup to reply.
  2. Take advantage of the fact that setState also accepts a function which references the current value of the state, instead of the value of state at render:

    import React, { useState } from "react";
    
    export default function ({}) {
      const [state, setState] = useState({ progress: 0, total: 0 });
    
      const wait = (duration) => () => new Promise((resolve) => setTimeout(resolve, duration));
    
      const startWork = async () => {
        const tasks = [wait(100), wait(200), wait(300), wait(400), wait(500)];
    
        setState({ ...state, progress: 0, total: tasks.length });
    
        for (const task of tasks) {
          // stop if cancelled
          //   if (state.total === 0) return;
    
          const data = await task();
    
          // update to reference current value of the state instead of value of state at render
          setState(curState => ({ ...curState, progress: curState.progress + 1 })); 
        }
      };
    
      const cancelWork = () => {
        setState({ ...state, total: 0 }); // maybe want to reset progress here too
      };
    
      return (
        <div>
          <p>
            {state.progress} / {state.total}
          </p>
          <button onClick={startWork}>Start</button>
          <button onClick={cancelWork}>Cancel</button>
        </div>
      );
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search