Async State Management in React – Thunk, Saga & RTK Query (2025 Guide)

Modern React applications often need to fetch data from APIs, handle async operations, and manage side effects.

While Redux provides predictable state management, handling async actions requires additional tools:

  • Redux Thunk – simplest way to handle async logic

  • Redux Saga – powerful for complex side effects

  • RTK Query – modern Redux Toolkit approach for API fetching

In this tutorial, we’ll explore each method, provide examples, and discuss when to use them.

Redux Thunk

Redux Thunk allows you to write async logic that interacts with the Redux store.

Installation

npm install redux-thunk

Example: Fetching Data with Thunk

// actions.js
export const fetchUsers = () => async (dispatch) => {
  dispatch({ type: "FETCH_USERS_REQUEST" });
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/users");
    const data = await response.json();
    dispatch({ type: "FETCH_USERS_SUCCESS", payload: data });
  } catch (error) {
    dispatch({ type: "FETCH_USERS_FAILURE", payload: error });
  }
};

Reducer Example:

const initialState = { users: [], loading: false, error: null };

export const usersReducer = (state = initialState, action) => {
  switch (action.type) {
    case "FETCH_USERS_REQUEST":
      return { ...state, loading: true };
    case "FETCH_USERS_SUCCESS":
      return { ...state, loading: false, users: action.payload };
    case "FETCH_USERS_FAILURE":
      return { ...state, loading: false, error: action.payload };
    default:
      return state;
  }
};

Usage in React:

import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { fetchUsers } from "./actions";

function UserList() {
  const dispatch = useDispatch();
  const { users, loading } = useSelector((state) => state.users);

  useEffect(() => {
    dispatch(fetchUsers());
  }, [dispatch]);

  if (loading) return <p>Loading...</p>;

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Thunk is simple and easy to learn, perfect for small-to-medium apps.

Redux Saga

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut elit tellus, luctus nec ullamcorper mattis, pulvinar dapibus leo.

Installation

npm install redux-saga

Example: Fetching Data with Saga

import { call, put, takeEvery } from "redux-saga/effects";

function* fetchUsersSaga() {
  try {
    const response = yield call(fetch, "https://jsonplaceholder.typicode.com/users");
    const data = yield response.json();
    yield put({ type: "FETCH_USERS_SUCCESS", payload: data });
  } catch (error) {
    yield put({ type: "FETCH_USERS_FAILURE", payload: error });
  }
}

export function* watchFetchUsers() {
  yield takeEvery("FETCH_USERS_REQUEST", fetchUsersSaga);
}

Reducer: Same as Thunk example.

Usage: Dispatch FETCH_USERS_REQUEST to trigger saga.

 Sagas are powerful for complex async flows, like retries, debouncing, and background tasks.

Redux Toolkit Query (RTK Query)

RTK Query is part of Redux Toolkit and provides automatic caching, fetching, and updating for APIs.

Installation

npm install @reduxjs/toolkit react-redux

Example: Creating an API Slice

import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

export const api = createApi({
  reducerPath: "api",
  baseQuery: fetchBaseQuery({ baseUrl: "https://jsonplaceholder.typicode.com/" }),
  endpoints: (builder) => ({
    getUsers: builder.query({
      query: () => "users",
    }),
  }),
});

export const { useGetUsersQuery } = api;

Using RTK Query in Component

function UserList() {
  const { data: users, error, isLoading } = useGetUsersQuery();

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error loading users.</p>;

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

RTK Query reduces boilerplate, automatically manages caching, loading, and error states.

Comparison: Thunk vs Saga vs RTK Query

FeatureThunkSagaRTK Query
ComplexityLowMedium-HighLow
BoilerplateMediumHighMinimal
Async HandlingSimple API callsComplex side effectsBuilt-in fetch + cache
Learning CurveEasyHardEasy
Best ForSmall appsComplex async flowsMost modern apps