React
dispatch method
async call
asynchronous programming
JavaScript

React dispatch a method in async call

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, dispatching during an async operation is normal, but the async work should not live inside the reducer itself. The reducer stays pure and synchronous, while an event handler, effect, or thunk-like layer performs the request and dispatches actions around it. Once you keep that separation clear, async state management becomes much easier to reason about.

Keep the Reducer Pure

A reducer should only compute the next state from the current state and an action. It should not call fetch, set timers, or trigger other side effects.

A common useReducer setup looks like this.

jsx
1import React, { useReducer } from "react";
2
3const initialState = {
4  loading: false,
5  user: null,
6  error: null,
7};
8
9function reducer(state, action) {
10  switch (action.type) {
11    case "load/start":
12      return { ...state, loading: true, error: null };
13    case "load/success":
14      return { ...state, loading: false, user: action.payload };
15    case "load/error":
16      return { ...state, loading: false, error: action.payload };
17    default:
18      return state;
19  }
20}

The reducer is only describing state transitions. That is exactly where it should stop.

Dispatch Around the Async Call

The async function lives in the component, where it can dispatch a start action before the request and a success or error action afterward.

jsx
1import React, { useReducer } from "react";
2
3const initialState = { loading: false, user: null, error: null };
4
5function reducer(state, action) {
6  switch (action.type) {
7    case "load/start":
8      return { ...state, loading: true, error: null };
9    case "load/success":
10      return { ...state, loading: false, user: action.payload };
11    case "load/error":
12      return { ...state, loading: false, error: action.payload };
13    default:
14      return state;
15  }
16}
17
18export default function UserLoader() {
19  const [state, dispatch] = useReducer(reducer, initialState);
20
21  async function loadUser() {
22    dispatch({ type: "load/start" });
23    try {
24      const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
25      const data = await response.json();
26      dispatch({ type: "load/success", payload: data });
27    } catch (error) {
28      dispatch({ type: "load/error", payload: String(error) });
29    }
30  }
31
32  return (
33    <div>
34      <button onClick={loadUser}>Load user</button>
35      {state.loading && <p>Loading...</p>}
36      {state.user && <p>{state.user.name}</p>}
37      {state.error && <p>{state.error}</p>}
38    </div>
39  );
40}

This is the core pattern for plain React state management with asynchronous work.

Use useEffect for Async Work Triggered by State or Props

If the request should happen automatically when the component mounts or when an input changes, use an effect. The dispatch pattern stays the same.

jsx
1import React, { useEffect, useReducer } from "react";
2
3const initialState = { loading: false, posts: [], error: null };
4
5function reducer(state, action) {
6  switch (action.type) {
7    case "posts/start":
8      return { ...state, loading: true, error: null };
9    case "posts/success":
10      return { ...state, loading: false, posts: action.payload };
11    case "posts/error":
12      return { ...state, loading: false, error: action.payload };
13    default:
14      return state;
15  }
16}
17
18export default function Posts() {
19  const [state, dispatch] = useReducer(reducer, initialState);
20
21  useEffect(() => {
22    let cancelled = false;
23
24    async function loadPosts() {
25      dispatch({ type: "posts/start" });
26      try {
27        const response = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=3");
28        const data = await response.json();
29        if (!cancelled) {
30          dispatch({ type: "posts/success", payload: data });
31        }
32      } catch (error) {
33        if (!cancelled) {
34          dispatch({ type: "posts/error", payload: String(error) });
35        }
36      }
37    }
38
39    loadPosts();
40    return () => {
41      cancelled = true;
42    };
43  }, []);
44
45  return <pre>{JSON.stringify(state, null, 2)}</pre>;
46}

The cancellation flag prevents a late dispatch from updating state after the component is gone.

Plain React Dispatch Is Not Redux Thunk Dispatch

This is where many questions come from. In plain useReducer, dispatch expects an action object. It does not automatically know how to execute an async function or promise. If you pass a function directly, React will not treat it like Redux thunk middleware does.

So the correct question is not “how do I dispatch an async method”. The correct question is “where should the async method live, and which actions should it dispatch before and after it runs”.

Model Async State Explicitly

The simplest useful async shape usually includes at least:

  • 'loading,'
  • the successful data,
  • and an error field.

That is better than a single boolean because the UI can distinguish “idle”, “loading”, “failed”, and “loaded”. Good action naming also makes bugs easier to trace later.

Common Pitfalls

  • Performing fetch or other side effects inside the reducer.
  • Dispatching after unmount because the async request completed late.
  • Treating useReducer dispatch as if thunk middleware were built in.
  • Collapsing all async outcomes into one flag instead of modeling loading, success, and error separately.
  • Forgetting that the reducer’s job is state transition logic, not request orchestration.

Summary

  • In React, async work belongs in event handlers, effects, or a separate async layer, not inside reducers.
  • Dispatch a start action before the request and success or error actions afterward.
  • 'useReducer dispatch expects plain actions by default.'
  • Guard against stale async completions when the component can unmount.
  • Keep reducers pure and let actions describe state changes clearly.

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.