Updating React State Inside Loops
Tags: react, javascript, webdev, tutorial
Published: 2023-04-24 21:57:28

I recently ran into a very nasty little conundrum with React.  I found a use-case where I felt it was necessary to update React state variables inside a loop.  I initially had no idea how vexing it would be.
<br/><br/>

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nf3tmxq04z9eta57kbha.jpg)
## State Management Under-The-Hood
Before I dive into this problem, there are a few basic facts that you need to understand about how React manages state variables.  This should be second nature to all of you salty old React vets out there.  But it's important to establish these ground rules before we dive in.

**React state is asynchronous**

When you update a React variable, the subsequent mutation occurs asynchronously.  You can prove it out with an example like this:

```javascript
export const MyComponent = () => {
  const [counter, setCounter] = useState(0);

  const increment = () => {
    setCounter(counter + 1);
    console.log('counter', counter);
  }

  return <>
    <div>
      Counter = {counter}
    </div>
    <button onClick={increment}>
      Increment
    </button>
  </>
}
```

From a UX perspective, the example above works just fine.  Every time you click `Increment`, the counter value updates.  However, the `console.log()` output will always be one update _behind_.  This happens because the update occurs asynchronously.  And when the code hits the `console.log()`, the value has not yet been updated.

**React _batches_ state updates**

React does a lot of work behind the scenes to optimize performance.  Normally, this optimization doesn't really phase us.  Because the typical flow is:

1. A user performs some action (like clicking a button).
1. The state variable is updated.
1. React's _reconciliation_ process is triggered.
1. This process realizes that there's now a diff between the virtual DOM and the actual DOM.
1. React updates the actual DOM to reflect the changes in the virtual DOM.

In the vast majority of circumstances this all happens so fast as to appear, to the naked eye, as instantaneous.  But the good folks who built React also put in some safeguards to protect against those instances where a developer tries to fire off a rapid series of state updates.

Of course, this optimization is, by and large, a very _good_ thing.  But it can be baffling when you feel that those successive updates are truly necessary.  Specifically, React goes out of its way to ensure that all state updates that are triggered _within a loop_ will not actually be rendered until the loop has finished running.
<br/><br/>


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/58cu04za6ezxwgfqn5vz.png)
## A Case Study For State Updates Inside A Loop
My website - https://paintmap.studio - does image/color manipulation.  Although the functionality works precisely as designed, this does present a few challenges:

1. Images have the potential to be large.  I _could_ introduce some artificial limits on the size of the image file (either in raw kilobytes or in height/width dimensions), but doing so would undermine much of the utility of the application.
<br/>

1. The images are not being uploaded and processed _on a server_.  It's a single page application with no backend component.  It runs entirely in the browser.  So I can't, for example, upload an image, commence some intensive processing, and then promise to email the user a link to the finished product when the process is complete.
<br/>

1. The amount of time it takes to process an image is largely dependent upon the processing _options_ that are chosen by the user.  For example, processing an image with the RGB color space is much faster than using Delta-E 2000.  Dithering an image takes more time than if dithering is avoided.  So in theory, I could introduce some artificial limits on the number of options available to the user.  But again, this would undermine much of the utility of the application itself.
<br/>

The simple fact is that sometimes I want to transform an image, of a certain size, and with a certain number of processor-intensive options, that's gonna take a little _time_ to complete.  When that happens, I don't want the user wondering if the application has crashed.  I want them to understand that everything is chugging along as-planned and their newly-processed image will be completed soon.
<br/><br/>


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z7bq2hf17bhq2mabnlu9.png)
## The Progress Bar
One of the most time-tested ways to let the user know that processing is continuing as-planned is to give them some kind of progress bar.  Ideally, that progress bar updates in real-time as the code works through its machinations.  

Of course, this is nothing new at all.  Applications have been providing this type of user feedback for decades.  But when I tried to implement this in React with Material UI's `<CircularProgress>` component, I ran into some really nasty headaches.

Because I don't want to confuse the matter by trying to drop you right into the middle of Paintmap Studio's code, I've instead created a simplified demo in CodeSandbox to illustrate the problem.  You can view that demo here:

https://codesandbox.io/s/update-react-state-in-a-loop-04b9ek
<br/><br/>


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/adacu95hzpfeanzmy7bx.png)
## The Problem - Illustrated
In the Code Sandbox demo, I've created an artificially-slow process to illustrate the issue.  When you load this demo, it gives you a single button on the screen titled `START SLOW PROCESS`.  The idea is that, once you click that button, a somewhat lengthy process will be triggered.  

But we don't want the user wondering whether the site's crashed. So once the button is clicked, we need to have an interstitial overlay shown on screen.  The overlay let's the user know that the process is underway, and ideally, gives them a dynamic progress indicator that lets them know how far the process is from being completed.

We'll start the illustration with `App.js`:

```javascript
// App.js
export const AppState = createContext({});

export const App = () => {
  const [progress, setProgress] = useState(0);
  const [showProcessing, setShowProcessing] = useState(false);

  return (
    <>
      <AppState.Provider
        value={{
          progress,
          setProgress,
          setShowProcessing,
          showProcessing
        }}
      >
        <UI />
      </AppState.Provider>
    </>
  );
};
```

`App.js` is mostly just a wrapper for the UX.  However, I'm using the Context API to establish some state variables that will be useful in the downstream code.  `progress` represents the completion percentage of our `SLOW PROCESS`.  `showProcessing` determines whether the progress interstitial should be shown.

Now let's take a look at `UI.js`:

```javascript
// UI.js
export const UI = () => {
  const { progress, showProcessing } = useContext(AppState);
  const { runSlowProcess } = useSlowProcess();

  const handleSlowProcessButton = () => runSlowProcess();

  return (
    <>
      <Backdrop
        sx={{
          color: "#fff",
          zIndex: (theme) => theme.zIndex.drawer + 1
        }}
        open={showProcessing}
      >
        <div className={"textAlignCenter"}>
          <Box
            sx={{
              display: "inline-flex",
              position: "relative"
            }}
          >
            <CircularProgress
              color={"success"}
              value={progress}
              variant={"determinate"}
            />
            <Box
              sx={{
                alignItems: "center",
                bottom: 0,
                display: "flex",
                justifyContent: "center",
                left: 0,
                position: "absolute",
                right: 0,
                top: 0
              }}
            >
              <Typography 
                color={"white"} 
                component={"div"} 
                variant={"caption"}
              >
                {`${progress}%`}
              </Typography>
            </Box>
          </Box>
          <br />
          Slow Process Running...
        </div>
      </Backdrop>
      <Button
        onClick={handleSlowProcessButton}
        size={"small"}
        variant={"contained"}
      >
        Start Slow Process
      </Button>
    </>
  );
};
```

First, the named components in the JSX, like `<Backdrop>`, `<Box>`, `<CircularProgress>`, `<Typography>`, and `<Button>` come from Material UI.

Second, the component leverages the `AppState` context that was established in `App.js`.  Specifically, we'll be using `showProcessing` to toggle the visibility of the interstitial layer.  And we'll be using `progress` to show the user how far the `SLOW PROCESS` is from completion.

Finally, this component also imports a custom Hook.  That Hook is called `useSlowProcess()`.  Let's take a look at that:

```javascript
// useSlowProcess.js
export const useSlowProcess = () => {
  const { setProgress, setShowProcessing } = useContext(AppState);

  const runSlowProcess = async () => {
    const startTime = Math.round(Date.now() / 1000);
    setProgress(0);
    setShowProcessing(true);
    for (let i = 1; i < 101; i++) {
      for (let j = 1; j < 1001; j++) {
        for (let k = 1; k < 1001; k++) {
          for (let l = 1; l < 1001; l++) {
            // do the stuffs
          }
        }
      }
      console.log(i);
      setProgress(i);
    }
    setShowProcessing(false);
    console.log(
      "Elapsed time:",
      Math.round(Date.now() / 1000) - startTime,
      "seconds"
    );
  };

  return {
    runSlowProcess
  };
};
```

`useSlowProcess()` returns a single function: `runSlowProcess()`.  `runSlowProcess()` does the following:

1. It establishes a `startTime` so we can calculate just how long the `SLOW PROCESS` actually takes.  (After playing with this repeatedly on Code Sandbox, I can tell you that it takes ~27 seconds to complete.)<br/>

1. It ensures that our `progress` variable starts at `0`.<br/>

1. It then sets `showProcessing` to `true` so the user will see the in-progress interstitial.<br/>

1. It then launches into the `SLOW PROCESS`.<br/>

1. The outer loop runs for 100 iterations.  This means that every completion of the outer loop essentially means that we've completed 1% of the overall process.<br/>

1. After each percentage is completed, we `console.log()` the current progress _and_ we update the `progress` variable.  Remember, that variable will be used to show the user how close we are to completion.<br/>

1. When the process is complete, we set `showProcessing` back to `false`.  This should remove the in-progress interstitial.<br/>

1. Finally, we `console.log()` the total number of seconds that it took to complete the `SLOW PROCESS`.<br/>

So what happens when we run this code???  Well... it's very disappointing.

When the user clicks the `START SLOW PROCESS` button, there is _no_ interstitial shown on screen.  This also means that the user sees no `<CircularProgress>` bar, with _no_ constantly-updated completion percentage.

But why does this happen?
<br/><br/>


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9pvzrbmmwhtt7cqvn9aa.jpeg)
## Batching Headaches
This is where we run head-first into the problems with React's batch updating of state variables.  React sees that we're setting `showProcessing` to `true` near the beginning of `runSlowProcess()` but we're _also_ setting it to `false` near the end of the process.  So the batched result is that it simply sets `showProcessing` to `false`.  Of course, it was _already_ `false` when we began the process.  So setting the previous value of `false` to... `false` results in the user never even seeing the in-progress interstitial.

Even if we solved that problem, the user would never see any of the percent updates to the `progress` variable displayed inside the `<CircularProgress>` component.  _Why?_  Because React sees that the state variable `progress` is being updated repeatedly through the outer `for` loop.  Thus, it _batches_ all of those updates into a single value.  Of course, this completely undermines the whole purpose of having a progress indicator in the first place.
<br/><br/>


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fxhk58twtufjk565moxg.png)
## Frustrating (Non)Help
Remember, the `<CircularProgress>` component comes from Material UI.  And Material UI has a ton of helpful documentation that's supposed to show you how to use their components.  So my first step was to go back to that documentation for help.  But... it was of no help whatsoever.

Here's the code sample that Material UI gives you to illustrate how you can update the `progress` value in the `<CircularProgress>` component:

```javascript
export default function CircularDeterminate() {
  const [progress, setProgress] = React.useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      setProgress((prevProgress) => (prevProgress >= 100 ? 0 : prevProgress + 10));
    }, 800);

    return () => {
      clearInterval(timer);
    };
  }, []);

  return (
    <CircularProgress variant="determinate" value={progress} />
  );
}
```

To be clear, Material UI's code sample "works".  I mean... it _does_ dynamically update the `progress` value inside the `<CircularProgress>` component.  But this is one of the few times that I found their documentation to be borderline useless.

You see, they're using `useEffect()`, in conjunction with `setInterval()`, to _blindly_ update the `progress` value based on nothing more than a predetermined delay of 800 milliseconds.  While that works fine for displaying a rote example of how the component renders, it does nothing to tell us how we can tie the `progress` value to an actual, _meaningful_ calculation based upon the true _progress_ of the process.

Faced with this very useless example, I then did what any lifelong developer would do:  I started googling.  But almost all of the posts I found on places like Stack Overflow were similarly useless.

The issue you encounter when you google this problem is that nearly every developer has the exact same "answer":
<br/>

> _Don't update React state variables in a loop._

<br/>While I understand that there are _many_ scenarios where you do want to avoid repeatedly updating React state inside a loop, the simple fact is that _this_ use case is the quintessential use case where you absolutely _should_ be updating React state inside a loop.  Because the `progress` value should, ideally, be calculated based upon the actual progress of the algorithm - **NOT** based upon some mindless `setInterval()` delay.

I actually encounter this sort of _non_-help all the time.  You're trying to solve some kind of sticky programming problem.  But rather than providing any _meaningful_ help, the mouth-breathers simply reply that you shouldn't be doing this at all.

It's like seeing someone's post about how they can't figure out how to cook a proper souffle without it collapsing and becoming a mess - and some smartass troll in the cooking forum responds by saying:
<br/>

> _You shouldn't be cooking souffles anyway.  Just make an omelette._

<br/>Wow. That's _sooooo_ helpful.  Thankfully, I did eventually solve the problem...
<br/><br/>


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qwxbakj9qkti7tbb0cy3.jpg)
## Promises To The Rescue
The key to solving this one is that you have to introduce _a delay_.  If you look at what's happening in Material UI's example, they're invoking a delay with `setInterval()`.  But the _real_ reason why their example "works" is because the `setInterval()` is embedded within `useEffect()`.  This creates a kind of feedback loop where the state variable is updated inside `useEffect()`, then the reconciliation process updates the DOM, which triggers another call to `useEffect()`, which then updates the variable again, and so on and so on...

Of course, in my example, I'm trying to track the progress of a function inside a Hook.  So it's not terribly useful to rely upon `useEffect()`.

But there's another way to invoke a delay without using `useTimeout()` or `useInterval()`.  You can use a _promise_.  Promises (and their associated `async/await` convention) basically knock React out of its batch update mentality.

The updated `useSlowProcess()` code looks like this:

```javascript
// useSlowProcess.js
export const useSlowProcess = () => {
  const { setProgress, setShowProcessing } = useContext(AppState);

  const runSlowProcess = async () => {
    const startTime = Math.round(Date.now() / 1000);
    setProgress(0);
    setShowProcessing(true);

    const delay = () => new Promise((resolve) => setTimeout(resolve, 0));

    for (let i = 1; i < 101; i++) {
      for (let j = 1; j < 1001; j++) {
        for (let k = 1; k < 1001; k++) {
          for (let l = 1; l < 1001; l++) {
            // do the stuffs
          }
        }
      }
      console.log(i);
      setProgress(i);
      await delay();
    }
    setShowProcessing(false);
    console.log(
      "Elapsed time:",
      Math.round(Date.now() / 1000) - startTime,
      "seconds"
    );
  };

  return {
    runSlowProcess
  };
};
```

Now when you click the `START SLOW PROCESS` button, the progress interstitial shows up on the screen, including the `<CircularProgress>` component with it's constantly updated `progress` indicator.

Notice a few things about this approach:

1. I invoke the delay directly _after_ I've updated the `progress` value inside the `for` loop. This has the side effect of knocking React out of the batch update process.<br/>

1. The _length_ of the delay is immaterial.  As you can see, I'm invoking a delay of **_ZERO_** milliseconds.  That may seem illogical, but it's all that's needed to trigger DOM updates in React.<br/>

**Conclusion**

I just wanna be clear that, in the vast majority of instances, it's truly a solid idea to _avoid_ doing repeated React state updates inside a loop.  But I wanted to highlight this use case because, when you're trying to give the user real-time info on the status of an ongoing process, it's one potential scenario where it makes total sense to do it.