1

OnClick I want to call a function that sets my loading state to true then do a for loop which will take more than one second and on the last loop set the loading state back to false but in my code the loading state doesn't change as expected. What do I have to fix?

import { useState } from "react"

const Test = () => {
  const [loading, setLoading] = useState(false)

  const someFunction = () => {
    setLoading(true)

    const someVeryBigArray = [...]
    for (let i = 0; i < someVeryBigArray.length; i++) {
      if (i === someVeryBigArray.length - 1) {
          setLoading(false)
      }
    }
  }

  return (
    <button onClick={someFunction} className={`${loading && "text-red-500"}`}>
      test
    </button>
  )
}

export default Test
  • Why doesn't it work as expected? I don't really see anything wrong with your code. Assuming the goal is for the text to turn red and then after the function finishes it turns back to normal. – Bas May 06 '22 at 02:50
  • In my code the loading state never sets to true. You can try it and console.log the loading state. –  May 06 '22 at 02:54
  • I tried it out of curiousity: https://codesandbox.io/s/romantic-rgb-vyr25l?file=/src/App.js, and it works fine? I use setTimeout to substitute the big array. – Bas May 06 '22 at 02:55
  • wait, are u doing console.log(loading) in the someFunction call? Yeah, that will always be false. setLoading is an async call. – Bas May 06 '22 at 02:57
  • I know that it's working with a setTimeout function but I'd like to achieve it without one and I use the console.log outside of the someFunction. –  May 06 '22 at 03:32
  • I showed this in the link above. You need to use the effect hook. – Bas May 06 '22 at 03:33

2 Answers2

0

You need to give the browser time to re-render. If you have a huge blocking loop, React won't be yielding control back to the browser so that it can repaint (or even to itself so that the component can run again with the new state).

While one approach would be to run the expensive function in an effect hook, after the new loading state has been rendered:

const Test = () => {
  const [running, setRunning] = useState(false)
  useEffect(() => {
    if (!running) return;
    const someVeryBigArray = [...]
    for (let i = 0; i < someVeryBigArray.length; i++) {
      // ...
    }
    setRunning(false);
  }, [running]);
  return (
    <button onClick={() => setRunning(true)} className={running && "text-red-500"}>
      test
    </button>
  )
}

A better approach would be to offload the expensive code to either the server, or to a web worker that runs on a separate thread, so as not to interfere with the UI view that React's presenting.

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • Could also get the big function to return a promise, and update state on `.then()`? – John Detlefs May 06 '22 at 06:48
  • 1
    That would work for a *normal* async function that simply needs to do something async (like an API request), but not really for something *expensive*, which is not the same thing. See https://stackoverflow.com/questions/49911319/promise-is-synchronous-or-asynchronous-in-node-js - if it blocks, even if it returns a Promise, it'll be a problem. – CertainPerformance May 06 '22 at 15:09
0

To be honest if in any case your loop is taking 1 second to run, then this will cost into you app's performance. And this is not the best way to do as well.

The better way would be, If your really want to replicate the delay in you app then you should use setTimeout() using which you delay some action. sharing a code snippet might help you.

JSX

import { useEffect, useState } from "react";
const Test = () => {
  const [loading, setLoading] = useState(false);
  let timevar = null;
  const someFunction = () => {
    setLoading(true);
    timevar = setTimeout(() => {
      setLoading(false); //this will run after 1 second
    }, 1000); //1000 ms = 1 second
  };
  useEffect(() => {
    return () => {
      //clear time out if in case component demounts during the 1 second
      clearTimeout(timevar);
    };
  });
  return (
    <button onClick={someFunction} className={`${loading && "text-red-500"}`}>
      test
    </button>
  );
};
export default Test;