skip to Main Content

I am performing a heavy computation in React, and I (1) don’t want the UI to block, and (2) want to display information about the process. I though that the useEffect hook was the right way to approach this, but I’m not getting the right result.

What i would expect to happen is that the "Loading" part would show, the percentage would run gradually from 0 to 100, and when it’s finished it says "Finished". What’s actually happening is that is starts at 0, then nothing during the whole of the counting, and then it jumps to "Finished" from nowhere.

What is the proper way to do this do both do the heavy lifting and also display the UI properly without blocking anything?

import { useEffect, useRef, useState } from "react";

export default function App() {
  const [isLoading, setIsLoading] = useState<boolean>(true);
  const [loadingPercentage, setLoadingPercentage] = useState<number>(0);
  const loadingRef = useRef<any>();

  useEffect(() => {
    if (!loadingRef.current) {
      return;
    }

    const MAX_COUNT = 1000000000;
    let threshold = 0;
    for (let count = 0; count < MAX_COUNT; ++count) {
      ///
      /// Simulate some heavy-lifting process with process counter
      ///
      if (count > threshold) {
        const percentage = (count / MAX_COUNT) * 100;
        setLoadingPercentage(percentage);
        // console.log(percentage); <-- This just demonstrates that the % set actually gets hit
        threshold += MAX_COUNT / 100;
      }
    }

    setLoadingPercentage(100);
    setIsLoading(false);
  }, [loadingRef.current]);

  return (
    <div className="App">
      <h1>Counting a whole lot of numbers</h1>
      {isLoading ? (
        <div>
          <h2 ref={loadingRef}>{"Loading... " + loadingPercentage + "%"}</h2>
        </div>
      ) : (
        <h2>Finished counting!</h2>
      )}
    </div>
  );
}

2

Answers


  1. Chosen as BEST ANSWER

    The following isn't necessarily a good answer, but it may be a least-bad answer for the purposes of this question.

    As @matt-morgan points out, the loop in the useEffect is blocking anything from happening on the UI thread, which is why the intermediate stages don't show up. As @David says, the "proper" way is probably to use Web Workers for this.

    The simpler yet non-perfect solution could be to use an async function, and build in some delay at some stages to give the UI thread some space to do it's thing. This does not completely unblock the UI like a background thread would, but at least it does give some space for the UI, so it will actually accomplish the desired UI update.

    import { useEffect, useRef, useState } from "react";
    
    export default function App() {
      const [isLoading, setIsLoading] = useState<boolean>(true);
      const [loadingPercentage, setLoadingPercentage] = useState<number>(0);
    
      useEffect(() => {
        function delay(delayMs: number) {
          return new Promise((res) => setTimeout(res, delayMs));
        }
    
        const doHeavyLiftingAsync = async () => {
          const MAX_COUNT = 1000000000;
          let threshold = 0;
          for (let count = 0; count < MAX_COUNT; ++count) {
            if (count > threshold) {
              const percentage = (count / MAX_COUNT) * 100;
              setLoadingPercentage(percentage);
              console.log(percentage);
              threshold += MAX_COUNT / 100;
              await delay(10);
            }
          }
          setLoadingPercentage(100);
          setIsLoading(false);
        };
        doHeavyLiftingAsync();
      }, []);
    
      return (
        <div className="App">
          <h1>Counting a whole lot of numbers</h1>
          {isLoading ? (
            <div>
              <h2>{"Loading... " + loadingPercentage + "%"}</h2>
            </div>
          ) : (
            <h2>Finished counting!</h2>
          )}
        </div>
      );
    }


  2. You can use setTimeout to show the progress asynchronously while the process is loading.

    But the best practice to heavy load processes is to use Web Workers that open a new thread in the browser.

    import React from 'react';
    
    export function App(props) {
      const [isLoading, setIsLoading] = React.useState(true);
      const [progressPercentage, setProgressPercentage] = React.useState(0);
      const step = 1
      const interval = 10
      const maxProgress = 100
      React.useEffect(() => {
        const updateProgress = () => setProgressPercentage(progressPercentage + step)
        if (progressPercentage < maxProgress) {
          setTimeout(updateProgress, interval)
        } else {
          setIsLoading(false);
        }
      },[progressPercentage]);
      return (
        <div className='App'>
          <h2>Example of a progress bar while loading content</h2>
          {isLoading ? (
                <div style={{borderColor:'red',borderWidth:2,borderStyle:'solid',backgrounColor:'black',padding:10,margin:50}} onClick={() => setProgressPercentage(0)}>
                  <div
                    style={{ backgroundColor:'white', color:'cyan', padding:10,width: progressPercentage + "%" }}
                  />
                  <p>{props.name}</p>
                </div>
            ) : (
              <h2>Finished loading!</h2>
          )}
        </div>
      );
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search