2

I have the following component. I want to break out of the loop when the button is clicked. How do I do this in React?

I tried everything I learned so far but nothing worked out for me.

import React, { useState } from 'react';

export default function Component() {
  const [abort, setAbort] = useState(false);
  const users = [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}];

  const insertOne = async (user) => {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(user), 1000);
    });
  };

  const handleInsert = async () => {
    for (const user of users) {
      if (abort) break;
      const insertedUser = await insertOne(user); // pretend this is uploading user to database
      console.log(insertedUser);
    }
  };

  return (
    <div>
      <button onClick={handleInsert}>Start Inserting Users</button>
      <button onClick={() => setAbort(true)}>Abort (why this doesn't work?)</button>
    </div>
  );
}

Code on stackblitz

Jim G.
  • 15,141
  • 22
  • 103
  • 166
Normal
  • 1,616
  • 15
  • 39
  • Comments are not for extended discussion; this conversation has been [moved to chat](https://chat.stackoverflow.com/rooms/248051/discussion-on-question-by-normal-how-to-break-out-of-an-async-for-loop-on-button). – Dharman Sep 14 '22 at 19:53

1 Answers1

3

What you're trying to do looks like this in vanilla JS:

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

let abort = false;
document
  .querySelector("button")
  .addEventListener("click", e => {
    abort = true;
  });

(async () => {
  for (const i of [...Array(100)].map((_, i) => i)) {
    if (abort) {
      break;
    }

    console.log(i);
    await sleep(1000);
  }
})();
<button>abort</button>

Now, in React, state is immutable, so from the perspective of the click handler that's running your loop, the value of abort will never change. It takes a re-render for that to happen once a state set call occurs.

Here's a reproduction of the problem:

<script type="text/babel" defer>
const sleep = ms => new Promise(r => setTimeout(r, ms));

const RepeaterWithAbort = () => {
  const [abort, setAbort] = React.useState(false);

  const start = async () => {
    for (const i of [...Array(100)].map((_, i) => i)) {
      if (abort) {
        break;
      }

      console.log(i, `abort = ${abort}`);
      await sleep(1000);
    }
  };

  return (
    <div>
      <button onClick={start}>
        Start
      </button>
      <button onClick={() =>
        console.log("abort!") || setAbort(true)
      }>
        Abort
      </button>
    </div>
  );
};

ReactDOM.createRoot(document.querySelector("#app"))
  .render(<RepeaterWithAbort />);
</script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div id="app"></div>

One solution is to use a ref:

<script type="text/babel" defer>
const sleep = ms => new Promise(r => setTimeout(r, ms));

const RepeaterWithAbort = () => {
  const abortRef = React.useRef(false);

  const start = async () => {
    for (const i of [...Array(100)].map((_, i) => i)) {
      if (abortRef.current) {
        break;
      }

      console.log(i);
      await sleep(1000);
    }
  };

  return (
    <div>
      <button onClick={start}>Start</button>
      <button onClick={() => abortRef.current = true}>
        Abort
      </button>
    </div>
  );
};

ReactDOM.createRoot(document.querySelector("#app"))
  .render(<RepeaterWithAbort />);
</script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div id="app"></div>

Also, there's nothing special about the for ... of loop here. This would work the same with a for ... in or C-style counter loop.

Related questions (but I think this thread is clearer):

ggorlen
  • 44,755
  • 7
  • 76
  • 106
  • Hmm, that's interesting. I didn't realize state worked like that-- it makes sense, but never run across a use-case where it mattered. – Nathan Sep 21 '22 at 16:14
  • You may also want to set `abortRef.current = false` at the beginning of `start`, so the process can happen multiple times. – Nathan Sep 21 '22 at 16:16
  • 1
    Yes, good suggestion, although the component is just a proof of concept, not intended to be a drop-in solution. Even with that change, you can spam the start button and trigger a bunch of timers, so more use-case-specific code is left to the reader to be added. – ggorlen Sep 21 '22 at 16:31
  • Good point. Ah, the joys of concurrency! – Nathan Sep 21 '22 at 20:08
  • @ggorlen--onLLMstrike Does this mean that it is impossible to abort while the sleep function is awaited? If I pass the sleep function 20 (20000 ms) seconds as an argument and try to abort once it started to await it, it will abort after the 20 seconds passed? Is that correct? – JackFrost Jun 14 '23 at 21:04