when createAsyncThunk and when use RTK query
system 2025-07-12T09:13:00Z: You are a expert on javascript and react
user 2025-07-15T14:20:50Z: teach me createAsyncThunk. why is it useful?
assistant 2025-07-15T14:24:13Z:
createAsyncThunk is a utility that ships with Redux Toolkit (RTK).
It lets you express a “side-effect” (usually an HTTP call) as one action creator that dispatches three separate redux actions for you:
pendingfulfilled(with the server’s response)rejected(with the thrown error)
You stop writing that boilerplate yourself (dispatching a _REQUEST → do fetch → dispatch _SUCCESS or _FAILURE) and get automatic loading / error flags, strong typing with TS, cancellation, optimistic updates, etc.
1. The problem createAsyncThunk solves
Classic hand-written:
// 1. action types
const FETCH_USER_REQUEST = 'FETCH_USER_REQUEST'
const FETCH_USER_SUCCESS = 'FETCH_USER_SUCCESS'
const FETCH_USER_FAILURE = 'FETCH_USER_FAILURE'
// 2. action creators
const fetchUserRequest = () => ({ type: FETCH_USER_REQUEST })
const fetchUserSuccess = user => ({ type: FETCH_USER_SUCCESS, payload: user })
const fetchUserFailure = error => ({ type: FETCH_USER_FAILURE, payload: error })
// 3. thunk action creator
export const fetchUser = id => async dispatch => {
dispatch(fetchUserRequest())
try {
const res = await axios.get(`/users/${id}`)
dispatch(fetchUserSuccess(res.data))
} catch (e) {
dispatch(fetchUserFailure(e.message))
}
}
That is 12 lines only for the plumbing.
With createAsyncThunk:
import { createAsyncThunk } from '@reduxjs/toolkit'
export const fetchUser = createAsyncThunk(
'users/fetchUser', // action prefix → creates 3 actions with /pending /fulfilled /rejected suffixes
async (id, { rejectWithValue }) => {
const res = await axios.get(`/users/${id}`)
return res.data // becomes action.payload for fulfilled
}
)
5 lines. The rest is generated for you.
2. How to use it (step-by-step)
- Generate the thunk:
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
export const fetchUser = createAsyncThunk(
'users/fetchUser',
async (userId, thunkAPI) => {
try {
const { data } = await axios.get(`/users/${userId}`)
return data
} catch (err) {
// Optional: pick off the server’s error message
return thunkAPI.rejectWithValue(err.response.data)
}
}
)
- Wire it up in a slice:
const usersSlice = createSlice({
name: 'users',
initialState: { entity: null, loading: false, error: null },
reducers: {}, // optional normal sync reducers
extraReducers: builder => {
builder
.addCase(fetchUser.pending, state => {
state.loading = true
state.error = null
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.loading = false
state.entity = action.payload
})
.addCase(fetchUser.rejected, (state, action) => {
state.loading = false
state.error = action.payload || action.error.message
})
}
})
export default usersSlice.reducer
- Dispatch from a React component:
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { fetchUser } from './usersSlice'
function User({ id }) {
const dispatch = useDispatch()
const { entity, loading, error } = useSelector(s => s.users)
useEffect(() => {
dispatch(fetchUser(id))
}, [id])
if (loading) return <Spinner />
if (error) return <p>{error}</p>
return <div>{entity.name}</div>
}
3. Why it’s more than just “saves lines”
- Automatic loading states: use
pending,fulfilled,rejectedto setloading/error– no race conditions. - Cancellable: you get an
AbortController.
thunkAPI.signallets you wirefetch(url, { signal }).
const fetchWithAbort = createAsyncThunk('todos/get', async (_, thunkAPI) => {
const res = await fetch('/todos', { signal: thunkAPI.signal })
return res.json()
})
- Serializing/thunk environment:
getState()→ read the whole store.dispatch()→ dispatch any other action or thunk.
-
Serializable check passes – all error objects are made serializable for Redux DevTools.
- TypeScript:
createAsyncThunk<User, string, { state: RootState }>()gives you correct action & payload types everywhere.
4. Common mini-patterns
- Transform the server payload before it hits reducers:
createAsyncThunk('todos/fetch', async () => {
const data = await api.fetchTodos()
return data.map(t => ({ ...t, isSelected: false }))
})
- Reject with a shaped error so your reducer can draw UI:
return rejectWithValue({ status: 422, errors: err.response.data.errors })
- Chain dependent calls:
const initApp = createAsyncThunk('app/init', async (_, thunkAPI) => {
await thunkAPI.dispatch(authenticate())
return thunkAPI.dispatch(fetchProfile()).unwrap() // returns the payload
})
TL;DR
createAsyncThunk is the RTK tool that kills 90 % of Redux async boilerplate while giving you strong typing, cancellation, error serialization and a clean separation between async side-effects and your pure reducers.
user 2025-07-15T14:25:27Z: when it will go to rejected? please tell me the exact situation.
assistant 2025-07-15T14:25:51Z:
There are only two strictly-defined code paths that make a createAsyncThunk jump into the rejected action:
| Location | Exact code the thunk executes | Outcome |
|---|---|---|
| 1️⃣ Your async payloadCreator throws synchronously OR an awaited Promise rejects and you do NOT catch it. | Anything that ends up like throw … or a rejected Promise bubbling out. |
Redux Toolkit automatically catches it → rejected action with action.error filled. |
2️⃣ You explicitly return rejectWithValue(payload) (after catching an error, validation failure, business rule, etc.). |
catch (e) { return rejectWithValue(e) } inside the payloadCreator. |
Toolkit immediately shortcuts to rejected action with action.payload === whatever you passed to rejectWithValue. |
That’s the whole list.
Any other branch (return someData, return undefined, even return null) lands in fulfilled.