You’ve learned how to manage local and global state, create slices, and use useSelector and useDispatch. Now it’s time to take a big step forward: fetching real data from an API.
In this lesson, we’ll learn how to handle asynchronous operations (like API requests) using createAsyncThunk — a powerful helper provided by Redux Toolkit.
Most real-world applications need to load data from somewhere — for example:
When we fetch data, we don’t get it instantly. The request could:
loading)data received)error returned)So we need to handle three states in our UI:
Redux Toolkit makes this simple.
createAsyncThunk — The Right Way to Handle AsynccreateAsyncThunk lets us:
Create a new slice file, for example:
src/features/pokemon/pokemonSlice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";export const fetchPokemon = createAsyncThunk("pokemon/fetchPokemon",async () => {const res = await fetch("https://pokeapi.co/api/v2/pokemon?limit=20");const data = await res.json();return data.results;});const pokemonSlice = createSlice({name: "pokemon",initialState: {items: [],status: "idle", // idle | loading | succeeded | failederror: null,},reducers: {},extraReducers: (builder) => {builder.addCase(fetchPokemon.pending, (state) => {state.status = "loading";}).addCase(fetchPokemon.fulfilled, (state, action) => {state.status = "succeeded";state.items = action.payload;}).addCase(fetchPokemon.rejected, (state) => {state.status = "failed";state.error = "Failed to fetch Pokémon.";});},});export default pokemonSlice.reducer;
| Part | Meaning |
|---|---|
fetchPokemon | The async function that fetches data |
.pending | Automatically triggered when the request starts |
.fulfilled | Triggered when data is successfully received |
.rejected | Triggered if the request fails |
Open your store.js:
import { configureStore } from "@reduxjs/toolkit";import pokemonReducer from "./features/pokemon/pokemonSlice";export const store = configureStore({reducer: {pokemon: pokemonReducer,},});
Open a UI component like App.jsx:
import { useSelector, useDispatch } from "react-redux";import { fetchPokemon } from "./features/pokemon/pokemonSlice";import { useEffect } from "react";function App() {const dispatch = useDispatch();const { items, status, error } = useSelector((state) => state.pokemon);useEffect(() => {dispatch(fetchPokemon());}, [dispatch]);return (<div style={{ padding: 20 }}><h1>Pokémon List</h1>{status === "loading" && <p>Loading...</p>}{status === "failed" && <p style={{ color: "red" }}>{error}</p>}{status === "succeeded" && (<ul>{items.map((p) => (<li key={p.name}>{p.name}</li>))}</ul>)}</div>);}export default App;
| Concept | Purpose |
|---|---|
createAsyncThunk | Handles async actions cleanly |
pending / fulfilled / rejected | Represent API call states |
status & error in UI | Make the app feel responsive & reliable |
This pattern will be used again and again as our app grows.