React
useState
async programming
JavaScript
hooks

Set useState hook in a async loop

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Using useState inside asynchronous loops is less about whether React allows it and more about how state updates are scheduled. The safe pattern is to either use functional updates for each asynchronous result or accumulate the results first and update state once, depending on whether intermediate renders are desirable.

The core problem is stale state, not the loop itself

React state setters do not update the state variable immediately. They schedule a re-render. In an async loop, that means code that reads the current state value can easily capture a stale snapshot.

Bad pattern:

jsx
1for (const item of items) {
2  const result = await fetchItem(item.id);
3  setResults([...results, result]);
4}

The results variable here is the value from the render that created the loop, not the latest state after each update.

Use functional updates when appending incrementally

If you want the UI to update after each asynchronous step, pass a function to the setter. React then gives you the latest committed state.

jsx
1import { useState } from "react";
2
3export default function Loader() {
4  const [results, setResults] = useState([]);
5
6  async function loadSequentially() {
7    const ids = [1, 2, 3];
8
9    for (const id of ids) {
10      const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
11      const item = await response.json();
12      setResults(prev => [...prev, item]);
13    }
14  }
15
16  return <button onClick={loadSequentially}>Load</button>;
17}

This works because prev is always the latest state, even if several async completions happen across different renders.

Update once when you do not need intermediate renders

If the UI does not need to show partial progress, accumulate locally and set state once at the end. This is often simpler and causes fewer re-renders.

jsx
1import { useState } from "react";
2
3export default function Loader() {
4  const [results, setResults] = useState([]);
5
6  async function loadAll() {
7    const ids = [1, 2, 3];
8    const collected = [];
9
10    for (const id of ids) {
11      const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
12      collected.push(await response.json());
13    }
14
15    setResults(collected);
16  }
17
18  return <button onClick={loadAll}>Load</button>;
19}

This is a good default when the only useful UI state is the final result.

Run async loops from effects or handlers, not during render

Do not start async work directly in the component body. Rendering must stay pure. Start the loop inside an event handler or inside useEffect.

jsx
1import { useEffect, useState } from "react";
2
3export default function Loader() {
4  const [results, setResults] = useState([]);
5
6  useEffect(() => {
7    let cancelled = false;
8
9    async function load() {
10      const ids = [1, 2, 3];
11      const collected = [];
12
13      for (const id of ids) {
14        const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
15        collected.push(await response.json());
16      }
17
18      if (!cancelled) {
19        setResults(collected);
20      }
21    }
22
23    load();
24    return () => {
25      cancelled = true;
26    };
27  }, []);
28
29  return <pre>{JSON.stringify(results, null, 2)}</pre>;
30}

The cancellation guard prevents state updates after unmount.

Consider parallel work when order does not require sequencing

Many async loops are sequential by habit, not by necessity. If the requests are independent, Promise.all is often faster and simpler.

jsx
1async function loadParallel(setResults) {
2  const ids = [1, 2, 3];
3  const data = await Promise.all(
4    ids.map(async id => {
5      const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
6      return response.json();
7    })
8  );
9
10  setResults(data);
11}

That avoids repeated state writes and shortens total wait time.

Common Pitfalls

  • Reading a stale state variable inside the async loop and appending from outdated data.
  • Starting the async loop during render instead of in an effect or event handler.
  • Updating state on every iteration when one final update would be simpler and cheaper.
  • Forgetting cleanup logic and calling setState after the component unmounts.
  • Using sequential awaits when the operations could safely run in parallel.

Summary

  • 'useState works with async loops, but stale state is the main hazard.'
  • Use functional updates when each async completion should update the UI.
  • Accumulate locally and call setState once when partial progress is unnecessary.
  • Start async loops from effects or handlers, never from render.
  • Use cleanup guards or cancellation when the component may unmount mid-request.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.