skip to Main Content

I’ve got an app with 2 values that user can change stored as state. When they’re changed I do some processing in useEffect and also store output of this processing in state and render it.

At this point everything is working perfectly fine, but this processing takes some time and I want to show some loading indicator. I want it to show after button click.

This is simplified version of my app and dumb implementation of this loading indicator – I know why this doesn’t work, I just don’t know how to write it correctly so I made this just to show what I’m trying to do. Here is the code:

function App() {
  const [value1, setValue1] = useState(0);
  const [value2, setValue2] = useState(0);
  const [output, setOutput] = useState(0);
  const [isLoading, setIsLoading] = useState(false); // setting true here won't work,
  // because I want to show loading indicator after user change value1 or value2
  // and not on inital load

  useEffect(() => {
    if (!value1 && !value2) {
      return;
    }
    setIsLoading(true);
    for (let i = 0; i < 1999999999; i++) {} // some long operations
    setOutput(value1 + value2);
    setIsLoading(false);
  }, [value1, value2]);

  return (
    <div>
      <button onClick={() => setValue1(value1 + 1)}>increment value1</button>
      <button onClick={() => setValue2(value2 + 1)}>increment value2</button>
      <div>
        {value1} + {value2} = {isLoading ? 'please wait...' : output}
      </div>
    </div>
  );
}

4

Answers


  1. You can start the value of isLoading equals true, so, the page will start on loading.

    function App() {
      const [value1, setValue1] = useState(0);
      const [value2, setValue2] = useState(0);
      const [output, setOutput] = useState();
      const [isLoading, setIsLoading] = useState(true);
    
      useEffect(() => {
        for (let i = 0; i < 1999999999; i++) {} // some long operations
        setOutput(value1 + value2);
        setIsLoading(false);
      }, [value1, value2]);
    
      return (
        <div>
          <button onClick={() => setValue1(value1 + 1)}>increment value1</button>
          <button onClick={() => setValue2(value2 + 1)}>increment value2</button>
          <div>
            {value1} + {value2} = {isLoading ? 'please wait...' : output}
          </div>
        </div>
      );
    }
    
    Login or Signup to reply.
  2. Try this piece of code:

    <div>
      <button
        onClick={() => {
          setIsLoading(true);
          setValue1(value1 + 1);
        }}
      >
        increment value1
      </button>
      <button
        onClick={() => {
          setIsLoading(true);
          setValue2(value2 + 1);
        }}
      >
        increment value2
      </button>
    

    Complete code is here: https://codesandbox.io/s/relaxed-hill-jy1bfm?file=/src/App.js:689-1018

    Login or Signup to reply.
  3. The problem is that it is asynchronous. There may be many different approaches to the solution, but for your simple example I have simple solution:

    export default function App() {
      const [value1, setValue1] = useState(0);
      const [value2, setValue2] = useState(0);
      const [output, setOutput] = useState(0);
      const isLoading = value1 + value2 !== output;
    
      useEffect(() => {
        if (!value1 && !value2) {
          return;
        }
    
        for (let i = 0; i < 1000; i++) {
          console.log(1);
        } // some long operations
        setOutput(value1 + value2);
      }, [value1, value2]);
    
      return (
        <div>
          <button onClick={() => setValue1(value1 + 1)}>increment value1</button>
          <button onClick={() => setValue2(value2 + 1)}>increment value2</button>
          <div>
            {value1} + {value2} = {isLoading ? "please wait..." : output}
          </div>
        </div>
      );
    }
    
    Login or Signup to reply.
  4. The problem is that between setting isLoading to true and setting it back to false (after the calculation-heavy operation) no rendering happened.

    Several approaches now come to my mind; and I’m not sure which one actually works as expected and (from those that do) which one I would pick; so I just share what’s on my mind:

    • approach 1 (adding it to the task queue of the main thread): only setting the loading flag synchronously (and thus returning from user code and handing the control flow back to the current render-run of react), and deliberately triggering the calculation in an asynchronous way

      useEffect(() => {
        if (!value1 && !value2) return;
        setIsLoading(true);
        setTimeout(() => {
          const result = heavyCalculation(value1, value2);
          setOutput(result);
          setIsLoading(false);
        });
      }, [value1, value2]);
      
    • approach 2 (adding it to the microtask queue of the main thread): turn it into micro tasks (a.k.a. Promises): when your useEffect just creates and starts a promise object and then "forgets" about it, the control flow is handed back to react. when a promise resolves and changes your component state react will do a re-render. But I suspect this might not bring any change, because afaik the microtask queue runs on the main thread and will run until empty, before react can schedule the re-rendering task. Disclaimer: promises are not my strong suit and I might have fudged that up here.

      useEffect(() => {
        if (!value1 && !value2) return;
        setIsLoading(true);
        Promise.resolve().then(() => {
            const result = heavyCalculation(value1, value2);
            setOutput(result);
            setIsLoading(false);
        });
      }, [value1, value2]);
      
    • approach 3 (using flushSync): ensuring the UI is rendered before your next statement. you are not allowed to call flushSync inside useEffect, therefore you need to place the call inside a new task or microtask

      useEffect(() => {
        if (!value1 && !value2) return;
      
        Promise.resolve()
          .then(() => flushSync(() => setIsLoading(true)))
          .then(() => {
            const result = heavyCalculation(value1, value2);
            setOutput(result);
            setIsLoading(false);
          });
      }, [value1, value2]);
      

      or

      useEffect(() => {
        if (!value1 && !value2) return;
      
        setTimeout(() => {
          flushSync(() => setIsLoading(true));
          const result = heavyCalculation(value1, value2);
          setOutput(result);
          setIsLoading(false);
        });
      }, [value1, value2]);
      
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search