L5: Async Thunk for Fetching
Create fetchListings async thunk to fetch data from the API
Now let's handle async operations in Redux! We'll create an async thunk to fetch listings from the API.
The Problem with Async in Redux
Redux reducers must be synchronous and pure:
// ❌ CAN'T do this in a reducer
reducers: {
fetchListings: async (state, action) => {
const response = await api.get('/listings'); // ❌ Async!
state.items = response.data;
}
}Why not?
- Reducers must be pure functions (same input → same output)
- Async operations have side effects
- Redux can't track async state changes properly
The Solution: Async Thunks
Redux Toolkit provides createAsyncThunk to handle async logic:
export const fetchListings = createAsyncThunk(
'listings/fetchListings',
async () => {
const response = await api.get('/listings'); // ✅ Async allowed here!
return response.data;
}
);What is a thunk?
A thunk is a function that returns a function. It delays execution:
// Regular function
const add = (a, b) => a + b;
add(2, 3); // Returns 5 immediately
// Thunk
const addThunk = (a, b) => () => a + b;
const delayed = addThunk(2, 3); // Returns a function
delayed(); // Call it later - returns 5Redux thunks let you dispatch async actions!
How Async Thunks Work
An async thunk automatically dispatches three actions:
Pending Action (Before API call)
dispatch(fetchListings());Automatically dispatches:
{ type: 'listings/fetchListings/pending' }Use this to set loading state.
Fulfilled Action (Success)
// API returns data successfullyAutomatically dispatches:
{
type: 'listings/fetchListings/fulfilled',
payload: [/* API data */]
}Use this to store the fetched data.
Rejected Action (Error)
// API call failsAutomatically dispatches:
{
type: 'listings/fetchListings/rejected',
error: { message: 'Network error' }
}Use this to store error message.
The thunk handles all three automatically! You just write the async logic.
What We're Building
We'll create fetchListings thunk that:
- Calls the API to fetch listings
- Returns the data on success
- Handles errors automatically
Step 1: Import Dependencies
Open src/state/slices/listingsSlice.js and update imports:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import api from '@/api';What did we add?
Step 2: Create Async Thunk
Add the async thunk before the slice definition:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import api from '@/api';
const initialState = {
items: [],
favorites: [],
status: 'idle',
error: null,
};
export const fetchListings = createAsyncThunk(
'listings/fetchListings',
async (filters = {}) => {
const response = await api.get('/listings', {
params: filters,
});
return response.data;
}
);
const listingsSlice = createSlice({
name: 'listings',
initialState,
reducers: {
toggleFavorite: (state, action) => {
const id = action.payload;
if (state.favorites.includes(id)) {
state.favorites = state.favorites.filter(favoriteId => favoriteId !== id);
} else {
state.favorites.push(id);
}
},
},
});
export const { toggleFavorite } = listingsSlice.actions;
export default listingsSlice.reducer;What's happening here?
Understanding the Async Flow
When you dispatch fetchListings():
Component Dispatches Thunk
dispatch(fetchListings({ search: 'beach' }));Pending Action Dispatched
Redux automatically dispatches:
{ type: 'listings/fetchListings/pending' }State updates to:
{ status: 'loading' } // We'll add this in next lessonAsync Function Executes
const response = await api.get('/listings', {
params: { search: 'beach' }
});
return response.data;API call happens, returns data.
Fulfilled Action Dispatched
Redux automatically dispatches:
{
type: 'listings/fetchListings/fulfilled',
payload: [/* listings data */]
}State updates to:
{
items: [/* listings data */],
status: 'succeeded'
}If the API call fails, rejected action is dispatched instead:
{
type: 'listings/fetchListings/rejected',
error: { message: 'Network error' }
}Thunk vs Regular Action
Regular Synchronous Action:
// Action creator
const addTodo = (text) => ({
type: 'todos/add',
payload: text
});
// Dispatch
dispatch(addTodo('Buy milk'));
// Reducer handles immediately
reducers: {
addTodo: (state, action) => {
state.push(action.payload);
}
}Async Thunk:
// Async thunk
const fetchTodos = createAsyncThunk(
'todos/fetch',
async () => {
const response = await api.get('/todos');
return response.data;
}
);
// Dispatch (same syntax!)
dispatch(fetchTodos());
// Handle with extraReducers (next lesson!)
extraReducers: (builder) => {
builder.addCase(fetchTodos.fulfilled, (state, action) => {
state.items = action.payload;
});
}Key differences:
- Thunks handle async operations
- Thunks generate 3 actions (pending/fulfilled/rejected)
- Thunks need
extraReducersnotreducers
Testing the Thunk
We can test the thunk is created correctly by adding a log:
export const fetchListings = createAsyncThunk(
'listings/fetchListings',
async (filters = {}) => {
console.log('Fetching listings with filters:', filters);
const response = await api.get('/listings', {
params: filters,
});
console.log('Fetch succeeded:', response.data);
return response.data;
}
);Remove these logs after testing! We'll handle the data properly in the next lesson.
Error Handling in Thunks
Async thunks handle errors automatically:
export const fetchListings = createAsyncThunk(
'listings/fetchListings',
async (filters = {}) => {
try {
const response = await api.get('/listings', {
params: filters,
});
return response.data;
} catch (error) {
// Error is automatically caught and dispatched as rejected action
throw error;
}
}
);You don't need try/catch! Redux Toolkit handles it:
- If promise resolves → fulfilled action
- If promise rejects → rejected action with error
For custom error messages:
export const fetchListings = createAsyncThunk(
'listings/fetchListings',
async (filters = {}, { rejectWithValue }) => {
try {
const response = await api.get('/listings', {
params: filters,
});
return response.data;
} catch (error) {
return rejectWithValue(error.response?.data || 'Failed to fetch listings');
}
}
);We'll keep it simple for now - default error handling works great!
Complete Code
Here's the complete slice with the async thunk:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import api from '@/api';
const initialState = {
items: [],
favorites: [],
status: 'idle',
error: null,
};
export const fetchListings = createAsyncThunk(
'listings/fetchListings',
async (filters = {}) => {
const response = await api.get('/listings', {
params: filters,
});
return response.data;
}
);
const listingsSlice = createSlice({
name: 'listings',
initialState,
reducers: {
toggleFavorite: (state, action) => {
const id = action.payload;
if (state.favorites.includes(id)) {
state.favorites = state.favorites.filter(favoriteId => favoriteId !== id);
} else {
state.favorites.push(id);
}
},
},
});
export const { toggleFavorite } = listingsSlice.actions;
export default listingsSlice.reducer;What's Next?
Great! The async thunk is created. In the next lesson, we'll:
- Add extraReducers - Handle pending/fulfilled/rejected actions
- Update state - Set loading/error/success states
- Store fetched data - Put listings in
state.items
✅ Lesson Complete! You've created an async thunk to fetch listings from the API!
Key Takeaways
- ✅ Reducers must be synchronous - Can't use async/await
- ✅
createAsyncThunkhandles async operations in Redux - ✅ Thunks automatically dispatch 3 actions: pending, fulfilled, rejected
- ✅ Return value becomes the fulfilled payload
- ✅ Errors automatically trigger rejected action
- ✅ Export thunk to dispatch from components
- ✅ Use
filtersparameter for flexible API queries