What Is Redux Thunk?
Redux Thunk is a middleware for Redux that allows you to write action creators that return a function instead of a plain action object.
Normally, Redux actions look like this:
{
type: "SET_USER",
payload: user
}
async code doesn't work directly: Redux would throw an error because it expects an object.
dispatch(async () => {
const data = await api.getUser()
})
But with thunk, you can return a function:
const fetchUser = () => {// Return an async function that receives dispatch as parameter
return async (dispatch) => {
const response = await api.getUser()
dispatch({
type: "SET_USER",
payload: response.data
})
}
}
Thunk acts as the bridge between async operations and Redux state updates.
The Problem Without Thunk
Imagine handling login directly inside a screen:
const handleLogin = async () => {
setLoading(true)
try {
const response = await api.login(email, password)
dispatch({
type: "LOGIN_SUCCESS",
payload: response.data
})
} catch (error) {
setError(error.message)
}
setLoading(false)
}
This approach causes problems:
- Components become bloated
- Logic is duplicated
- Reusability decreases
- Testing becomes harder
- UI and business logic become tightly coupled
The Same Logic Using Thunk:
export const loginUser = (email, password) => {
return async (dispatch) => {
dispatch(setLoading(true))
try {
const response = await api.login(email, password)
dispatch(loginSuccess(response.data))
} catch (error) {
dispatch(loginError(error.message))
} finally {
dispatch(setLoading(false))
}
}
}
Then inside the component:
dispatch(loginUser(email, password))
Now the component becomes clean and focused only on UI.
Redux Toolkit createAsyncThunk
Modern Redux apps should prefer Redux Toolkit. Instead of manually writing thunk boilerplate.
The Structure Usually:
- Service Layer: Handles API only.
// services/userService.js
export const getProducts = async () => {
const response = await fetch(API_URL)
return response.json()
}
- Thunk Layer: Handles async Redux logic.
// store/thunks/userThunk.js
export const fetchProducts = createAsyncThunk(
"products/fetchProducts", // action type prefix. // sliceName/actionName
async () => {
return await getProducts()
}
)
"products/fetchProducts" It is simply:a unique Redux action identifier prefix used to generate async action types automatically.
- Slice Layer: Handles state updates.
import { createSlice } from "@reduxjs/toolkit"
import { fetchProducts } from "./productThunk"
const productSlice = createSlice({
name: "products",
initialState: {
products: [],
loading: false,
error: null
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchProducts.pending, (state) => { /
state.loading = true
})
.addCase(fetchProducts.fulfilled, (state, action) => {
//internally matches:products/fetchProducts/fulfilled
state.loading = false
state.products = action.payload
})
.addCase(fetchProducts.rejected, (state, action) => {
state.loading = false
state.error = action.error.message
})
}
})
export default productSlice.reducer
Benefits:
- Cleaner syntax
- Built-in pending/fulfilled/rejected states
- Less boilerplate
- Better TypeScript support
Important Concept
createAsyncThunk is NOT mainly about API calls.
It is about:
Managing async operations that affect global state. API calls are just the most common example.
Better understanding:
Thunk = async business logic connected to Redux state
That business logic may include:
- API calls
- caching
- conditional fetching
- retries
- authentication
- reading current state
- dispatching multiple actions
Top comments (0)