React
setState
async
JavaScript
state management

React, setState with async updater parameter?

Interview Questions practice on Codemia

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

Browse interview questions

Introduction

In React class components, setState accepts either an object or an updater function, but that updater function is not meant to be async. The updater must stay synchronous and pure because React uses it to calculate the next state from the previous state and props, not to run asynchronous side effects.

The Correct Updater Pattern

When the next state depends on previous state, use the functional updater form:

jsx
1class Counter extends React.Component {
2  state = { count: 0 };
3
4  increment = () => {
5    this.setState((prevState) => ({
6      count: prevState.count + 1,
7    }));
8  };
9
10  render() {
11    return <button onClick={this.increment}>{this.state.count}</button>;
12  }
13}

This function runs synchronously from React’s perspective. It receives the previous state and returns the next partial state object.

Why an async Updater Is the Wrong Tool

It is tempting to write something like this:

jsx
1this.setState(async (prevState) => {
2  const value = await fetchSomething();
3  return { count: prevState.count + value };
4});

That is not the intended contract. An async function returns a promise, but setState expects the updater to return an object or null, not a promise that resolves later. Even if the code type-checks loosely in some setups, it is conceptually wrong and leads to confusing behavior.

The updater function should be treated as pure state calculation, not as an asynchronous workflow hook.

Do the Async Work Outside setState

If you need asynchronous data, perform it first, then call setState with the result:

jsx
1class UserPanel extends React.Component {
2  state = {
3    loading: false,
4    user: null,
5  };
6
7  loadUser = async () => {
8    this.setState({ loading: true });
9
10    const response = await fetch("/api/user");
11    const user = await response.json();
12
13    this.setState({
14      user,
15      loading: false,
16    });
17  };
18
19  render() {
20    return <button onClick={this.loadUser}>Load user</button>;
21  }
22}

This keeps the responsibilities clear:

  • async work happens in an event handler or lifecycle method
  • state calculation stays synchronous inside setState

Combine Previous State with Async Results Safely

Sometimes you need both asynchronous data and previous state. In that case, await first, then use the functional updater:

jsx
1class ScoreBoard extends React.Component {
2  state = { total: 0 };
3
4  addRemoteValue = async () => {
5    const response = await fetch("/api/value");
6    const { amount } = await response.json();
7
8    this.setState((prevState) => ({
9      total: prevState.total + amount,
10    }));
11  };
12}

That is the right composition. The async part fetches the external value. The updater part calculates the next state from the previous state.

Remember That setState Is Scheduled

Another source of confusion is the phrase “setState is async”. That does not mean the updater function should be async. It means React schedules the state update rather than mutating this.state immediately in place.

If you need code to run after the state update has been applied in a class component, use the second callback parameter:

jsx
1this.setState(
2  { loading: false },
3  () => {
4    console.log("state updated");
5  }
6);

That callback is for post-update side effects. It is still separate from the updater function itself.

Hooks Are Different, but the Rule Still Applies

Newer React code often uses hooks instead of class components, but the same principle holds. The updater passed to setCount in hooks should also be synchronous:

jsx
setCount((prev) => prev + 1);

Async work belongs around state updates, not inside the updater calculation.

Common Pitfalls

  • Marking the setState updater function as async and expecting React to await it.
  • Doing fetches or timers inside the updater instead of in an event handler, effect, or lifecycle method.
  • Forgetting to use the functional updater when the next state depends on previous state.
  • Misunderstanding “setState is asynchronous” as meaning the updater function itself should return a promise.
  • Mixing too many responsibilities into one setState call and making state flow hard to reason about.

Summary

  • The setState updater function should be synchronous and pure.
  • Do asynchronous work before calling setState, not inside the updater.
  • Use the functional updater when the next state depends on previous state.
  • Use the second setState callback for post-update side effects in class components.
  • Treat async workflows and state calculation as separate steps for predictable React code.

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.