CougarHunter
CougarHunter

Reputation: 11

React doesn't re-render component after state changes when main thread is queued up with other operations?

Code and More illustration - here

I'm working on a computation-heavy web app and I want users to know the progression on the computation so that the app doesn't appear as stale. Basically I have three messages to indicate in state of the computation -

I created a displayMsg state in the App component, which get sent down as a prop to a child component Copy. The setter method for displayMsg, setDisplayMsg, is invoked in different computation stages. However, I'm only seeing Loading and 80% Finished while the intermediate stage, 50% Finished, is skipped.

Specifically, my App component is has useEffect as follow -

  React.useEffect(() => {
    setDisplayMsg("50% Finished");

    simulateHeavyComputation(3000);

    setDisplayMsg("80% Finished");
  }, []);

where

  const simulateHeavyComputation = (sleepDuration) => {
    var now = new Date().getTime();
    while (new Date().getTime() < now + sleepDuration) {
      /* do nothing */
    }
  };

In the child component, the message 50% Finished was successfully sent down as prop but the actual DOM was never rendered.

https://raw.githubusercontent.com/wchen408/js_react_status_indicator/main/asset/actual_behavior.gif

However, what I'm really expecting to see is

https://raw.githubusercontent.com/wchen408/js_react_status_indicator/main/asset/expected_behavior.gif

I wonder if there's anyway for me to delegate the work done in simulateHeavyComputation in a worker thread so that the main thread is not blocked from rendering the DOM. Thank you so much for reading!

Upvotes: 1

Views: 212

Answers (2)

Mike B.
Mike B.

Reputation: 133

I think what you are trying to achieve is a real-time progress bar. For that you need some kind of data (maybe from a server) that you use as a dependency for your useEffect hook.

In the code below, I'm simulating this data with computation() and setTimeout.

    const [displayMsg, setDisplayMsg] = React.useState("Loading");
    const [progress, setProgress] = React.useState(1);

    computation();

    function computation() {
        const prog = setTimeout(() => {
          setProgress(() => progress + 1);
        }, 40);
        
        if (progress === 100) {
          clearTimeout(prog);
    }
  }

Now you could return the progress with string templating like you see below. It would show the real percentage instead of waiting in between each step 50%->80%. So the user would always know that the app is still processing:

return (
    <div className="App">
      <h1>Message</h1>
      <h2>`Progress: ${progress}%`</h2>
    </div>
  );
}

If you want to use a message system or other side effects like filling up a visual progress bar, then you would use the useEffect hook similar to your example. However, if you fill [] as the dependency then the useEffect hook will only execute on the first render. In my example above, you can use the progress useState as the dependency and display messages based on the state. See below:

React.useEffect(() => {
    if (progress <= 50) {
      setDisplayMsg(() => "Working up to 50%");
    } else if (progress >= 50 && progress <= 80) {
      setDisplayMsg(() => "Working up to 80%");
    } else if (progress >= 80 && progress <= 99) {
      setDisplayMsg(() => "Finishing up");
    } else if (progress === 100) {
      setDisplayMsg(() => "DONE");
    }
  }, [progress]);

You can test the code here

If you utilize socket.io on your backend then you could divide the package into numerical chunks and receive the data on the client side as below while setting your progress:

socket.on("countedData", (count)=> {
    setProgress(() => count)
})

I hope this helps. Here is a progress bar package that you could use to have a visual progress bar

Upvotes: 0

Ilanus
Ilanus

Reputation: 6928

This part creates infinite loop in your application: while (new Date().getTime() < now + sleepDuration)

I would solve this by using async/await with a timeout Promise

First, create a sleep function that will resolve after x amount of milliseconds

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

Add a function to start the simulation:

const startSimulation = async () => {
  await sleep(1000);
  // update msg after 1s
  setDisplayMsg("50% Finished");

  // update msg after 3s
  await sleep(3000);
  setDisplayMsg("80% Finished");
};

And execute in useEffect

React.useEffect(() => {
  startSimulation();
}, []);

https://codesandbox.io/s/rough-wind-1k6r6?file=/src/App.js:991-1302

Upvotes: 0

Related Questions