Advanced Redux — Side Effects, Async Tasks & ThunksEntry 6 of 9
createAsyncThunk: The Modern Way
RTK's helper that wraps an async function and automatically dispatches pending, fulfilled, and rejected actions, handled in the slice's extraReducers.
Notes
- No hand-written loading/success/failure actions. RTK generates
users/fetchUsers/pending,/fulfilled, and/rejected. - The returned value becomes
action.payloadinfulfilled. A thrown error lands inaction.error.messageinrejected. - Track request state with a
statusfield:'idle' | 'loading' | 'succeeded' | 'failed'. - Guarding with
status === 'idle'before dispatching prevents duplicate fetches when several components mount. - Use
rejectWithValueto return a custom error payload instead of a plain message.
Resulting flow:
- Component dispatches
fetchUsers(). - RTK dispatches
users/fetchUsers/pending. - The API resolves or fails; RTK dispatches
fulfilledorrejected. extraReducersupdate state.- The UI re-renders from the new state.
What / Why / When
- What
Standardized async lifecycle for one request.
- Why
Less boilerplate than custom thunks, consistent loading/error handling, fully typed.
- When
A request maps to one piece of state (load users, load products). Use a custom thunk (Entry 5) for multi-step flows.
Code compare
// features/users/api.tsexport type User = { id: number; name: string };​export async function getUsers(): Promise<User[]> { const res = await fetch("https://jsonplaceholder.typicode.com/users"); if (!res.ok) throw new Error("Failed to fetch users"); return res.json();}// features/users/users.service.ts@Injectable({ providedIn: "root" })export class UsersService { private http = inject(HttpClient);​ getUsers() { return this.http.get<User[]>("https://jsonplaceholder.typicode.com/users"); }}Differences and gotchas
createAsyncThunkgenerates three actions from one call. NgRx needs three declared actions plus an effect, but the action names document intent.exhaustMapignores newloadactions while a request is in flight, which replaces thestatus === 'idle'guard as the main protection against duplicates.- In React StrictMode (development), effects run twice on mount. The
idleguard plus RTK'sconditionoption prevents a double request.
Quick reference
| Task | Redux Toolkit | NgRx |
|---|---|---|
| Define request | createAsyncThunk('users/fetchUsers', fn) | Action group + createEffect() |
| Lifecycle actions | Auto: pending / fulfilled / rejected | Declared: Load / Load Success / Load Failure |
| Handle in state | extraReducers + builder.addCase | on() in createReducer |
| Error value | action.error.message / rejectWithValue | Failure action props |
| Prevent duplicates | status === 'idle' / condition | exhaustMap |