0
# Async Operations
1
2
Redux Toolkit's async thunk functionality provides a streamlined approach to handling asynchronous logic with automatic loading states, error management, and type safety.
3
4
## Capabilities
5
6
### Create Async Thunk
7
8
Creates an async action creator that handles pending, fulfilled, and rejected states automatically.
9
10
```typescript { .api }
11
/**
12
* Creates async action creator for handling asynchronous logic
13
* @param typePrefix - Base action type string (e.g., 'users/fetchById')
14
* @param payloadCreator - Async function that returns the data or throws an error
15
* @param options - Configuration options
16
* @returns AsyncThunk with pending/fulfilled/rejected action creators
17
*/
18
function createAsyncThunk<
19
Returned,
20
ThunkArg = void,
21
ThunkApiConfig extends AsyncThunkConfig = {}
22
>(
23
typePrefix: string,
24
payloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg, ThunkApiConfig>,
25
options?: AsyncThunkOptions<ThunkArg, ThunkApiConfig>
26
): AsyncThunk<Returned, ThunkArg, ThunkApiConfig>;
27
28
interface AsyncThunk<Returned, ThunkArg, ThunkApiConfig extends AsyncThunkConfig> {
29
/** Action creator for pending state */
30
pending: ActionCreatorWithPreparedPayload<
31
[string, ThunkArg, AsyncThunkOptions<ThunkArg, ThunkApiConfig>?],
32
undefined,
33
string,
34
never,
35
{ arg: ThunkArg; requestId: string; requestStatus: "pending" }
36
>;
37
38
/** Action creator for successful completion */
39
fulfilled: ActionCreatorWithPreparedPayload<
40
[Returned, string, ThunkArg, AsyncThunkOptions<ThunkArg, ThunkApiConfig>?],
41
Returned,
42
string,
43
never,
44
{ arg: ThunkArg; requestId: string; requestStatus: "fulfilled" }
45
>;
46
47
/** Action creator for rejection/error */
48
rejected: ActionCreatorWithPreparedPayload<
49
[unknown, string, ThunkArg, AsyncThunkOptions<ThunkArg, ThunkApiConfig>?, string?, SerializedError?],
50
undefined,
51
string,
52
SerializedError,
53
{ arg: ThunkArg; requestId: string; requestStatus: "rejected"; aborted?: boolean; condition?: boolean }
54
>;
55
56
/** Matcher for any settled action (fulfilled or rejected) */
57
settled: ActionMatcher<ReturnType<AsyncThunk<Returned, ThunkArg, ThunkApiConfig>['fulfilled']> | ReturnType<AsyncThunk<Returned, ThunkArg, ThunkApiConfig>['rejected']>>;
58
59
/** The base type prefix */
60
typePrefix: string;
61
}
62
63
type AsyncThunkPayloadCreator<Returned, ThunkArg, ThunkApiConfig extends AsyncThunkConfig> = (
64
arg: ThunkArg,
65
thunkAPI: GetThunkAPI<ThunkApiConfig>
66
) => Promise<Returned> | Returned;
67
68
interface AsyncThunkConfig {
69
/** Return type of getState() */
70
state?: unknown;
71
/** Type of dispatch */
72
dispatch?: Dispatch;
73
/** Type of extra argument passed to thunk middleware */
74
extra?: unknown;
75
/** Return type of rejectWithValue's first argument */
76
rejectValue?: unknown;
77
/** Type passed into serializeError's first argument */
78
serializedErrorType?: unknown;
79
/** Type of pending meta's argument */
80
pendingMeta?: unknown;
81
/** Type of fulfilled meta's argument */
82
fulfilledMeta?: unknown;
83
/** Type of rejected meta's argument */
84
rejectedMeta?: unknown;
85
}
86
```
87
88
**Usage Examples:**
89
90
```typescript
91
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
92
93
// Basic async thunk
94
const fetchUserById = createAsyncThunk(
95
'users/fetchById',
96
async (userId: string) => {
97
const response = await userAPI.fetchById(userId);
98
return response.data;
99
}
100
);
101
102
// With error handling
103
const fetchUserByIdWithError = createAsyncThunk(
104
'users/fetchByIdWithError',
105
async (userId: string, { rejectWithValue }) => {
106
try {
107
const response = await userAPI.fetchById(userId);
108
return response.data;
109
} catch (err: any) {
110
return rejectWithValue(err.response?.data || err.message);
111
}
112
}
113
);
114
115
// With state access and extra arguments
116
interface ThunkApiConfig {
117
state: RootState;
118
extra: { api: ApiClient; analytics: Analytics };
119
rejectValue: { message: string; code?: number };
120
}
121
122
const fetchUserData = createAsyncThunk<
123
User,
124
string,
125
ThunkApiConfig
126
>(
127
'users/fetchData',
128
async (userId, { getState, extra, rejectWithValue, signal }) => {
129
const { user } = getState();
130
131
// Check if already loading
132
if (user.loading) {
133
return rejectWithValue({ message: 'Already loading' });
134
}
135
136
// Use extra arguments
137
const { api, analytics } = extra;
138
analytics.track('user_fetch_started');
139
140
try {
141
const response = await api.fetchUser(userId, { signal });
142
return response.data;
143
} catch (err: any) {
144
if (err.name === 'AbortError') {
145
return rejectWithValue({ message: 'Request cancelled' });
146
}
147
return rejectWithValue({
148
message: err.message,
149
code: err.status
150
});
151
}
152
}
153
);
154
155
// Using in a slice
156
const userSlice = createSlice({
157
name: 'user',
158
initialState: {
159
entities: {} as Record<string, User>,
160
loading: false,
161
error: null as string | null
162
},
163
reducers: {},
164
extraReducers: (builder) => {
165
builder
166
.addCase(fetchUserById.pending, (state) => {
167
state.loading = true;
168
state.error = null;
169
})
170
.addCase(fetchUserById.fulfilled, (state, action) => {
171
state.loading = false;
172
state.entities[action.meta.arg] = action.payload;
173
})
174
.addCase(fetchUserById.rejected, (state, action) => {
175
state.loading = false;
176
state.error = action.error.message || 'Failed to fetch user';
177
});
178
}
179
});
180
```
181
182
### Async Thunk Options
183
184
Configure thunk behavior with conditional execution, custom error serialization, and metadata.
185
186
```typescript { .api }
187
/**
188
* Options for configuring async thunk behavior
189
*/
190
interface AsyncThunkOptions<ThunkArg, ThunkApiConfig extends AsyncThunkConfig> {
191
/** Skip execution based on condition */
192
condition?(arg: ThunkArg, api: GetThunkAPI<ThunkApiConfig>): boolean | undefined;
193
194
/** Whether to dispatch rejected action when condition returns false */
195
dispatchConditionRejection?: boolean;
196
197
/** Custom error serialization */
198
serializeError?(x: unknown): GetSerializedErrorType<ThunkApiConfig>;
199
200
/** Custom request ID generation */
201
idGenerator?(arg: ThunkArg): string;
202
203
/** Add metadata to pending action */
204
getPendingMeta?(
205
base: { arg: ThunkArg; requestId: string },
206
api: GetThunkAPI<ThunkApiConfig>
207
): GetPendingMeta<ThunkApiConfig>;
208
209
/** Add metadata to fulfilled action */
210
getFulfilledMeta?(
211
base: { arg: ThunkArg; requestId: string },
212
api: GetThunkAPI<ThunkApiConfig>
213
): GetFulfilledMeta<ThunkApiConfig>;
214
215
/** Add metadata to rejected action */
216
getRejectedMeta?(
217
base: { arg: ThunkArg; requestId: string },
218
api: GetThunkAPI<ThunkApiConfig>
219
): GetRejectedMeta<ThunkApiConfig>;
220
}
221
```
222
223
**Usage Examples:**
224
225
```typescript
226
// Conditional execution
227
const fetchUserById = createAsyncThunk(
228
'users/fetchById',
229
async (userId: string) => {
230
return await userAPI.fetchById(userId);
231
},
232
{
233
// Skip if user already exists
234
condition: (userId, { getState }) => {
235
const { users } = getState() as RootState;
236
return !users.entities[userId];
237
},
238
239
// Custom ID generation
240
idGenerator: (userId) => `user-${userId}-${Date.now()}`,
241
242
// Add metadata to pending action
243
getPendingMeta: ({ arg }, { getState }) => ({
244
startedTimeStamp: Date.now(),
245
source: 'user-component'
246
})
247
}
248
);
249
250
// Custom error serialization
251
const fetchWithCustomErrors = createAsyncThunk(
252
'data/fetch',
253
async (id: string) => {
254
throw new Error('API Error');
255
},
256
{
257
serializeError: (error: any) => ({
258
message: error.message,
259
code: error.code || 'UNKNOWN',
260
timestamp: Date.now()
261
})
262
}
263
);
264
```
265
266
### Thunk API Object
267
268
The thunk API object provides access to Redux store methods and additional utilities.
269
270
```typescript { .api }
271
/**
272
* ThunkAPI object passed to async thunk payload creators
273
*/
274
interface BaseThunkAPI<S, E, D extends Dispatch, RejectedValue, RejectedMeta, FulfilledMeta> {
275
/** Function to get current state */
276
getState(): S;
277
278
/** Enhanced dispatch function */
279
dispatch: D;
280
281
/** Extra argument from thunk middleware */
282
extra: E;
283
284
/** Unique request identifier */
285
requestId: string;
286
287
/** AbortSignal for request cancellation */
288
signal: AbortSignal;
289
290
/** Reject with custom value */
291
rejectWithValue(value: RejectedValue, meta?: RejectedMeta): RejectWithValue<RejectedValue, RejectedMeta>;
292
293
/** Fulfill with custom value */
294
fulfillWithValue<FulfilledValue>(value: FulfilledValue, meta?: FulfilledMeta): FulfillWithMeta<FulfilledValue, FulfilledMeta>;
295
}
296
297
type GetThunkAPI<ThunkApiConfig> = BaseThunkAPI<
298
GetState<ThunkApiConfig>,
299
GetExtra<ThunkApiConfig>,
300
GetDispatch<ThunkApiConfig>,
301
GetRejectValue<ThunkApiConfig>,
302
GetRejectedMeta<ThunkApiConfig>,
303
GetFulfilledMeta<ThunkApiConfig>
304
>;
305
```
306
307
**Usage Examples:**
308
309
```typescript
310
const complexAsyncThunk = createAsyncThunk(
311
'complex/operation',
312
async (
313
{ id, data }: { id: string; data: any },
314
{ getState, dispatch, extra, requestId, signal, rejectWithValue, fulfillWithValue }
315
) => {
316
const state = getState() as RootState;
317
318
// Check current state
319
if (state.complex.processing) {
320
return rejectWithValue('Already processing');
321
}
322
323
// Dispatch other actions
324
dispatch(startProcessing());
325
326
// Use extra arguments (API client, etc.)
327
const { apiClient } = extra as { apiClient: ApiClient };
328
329
try {
330
// Check for cancellation
331
if (signal.aborted) {
332
throw new Error('Operation cancelled');
333
}
334
335
const result = await apiClient.processData(id, data, { signal });
336
337
// Return with custom metadata
338
return fulfillWithValue(result, {
339
requestId,
340
processedAt: Date.now()
341
});
342
} catch (error: any) {
343
if (error.name === 'AbortError') {
344
return rejectWithValue('Cancelled', { reason: 'user_cancelled' });
345
}
346
return rejectWithValue(error.message, { errorCode: error.code });
347
} finally {
348
dispatch(endProcessing());
349
}
350
}
351
);
352
```
353
354
### Result Unwrapping
355
356
Utilities for working with async thunk results and handling fulfilled/rejected outcomes.
357
358
```typescript { .api }
359
/**
360
* Unwraps the result of an async thunk action
361
* @param action - The dispatched async thunk action
362
* @returns Promise that resolves with payload or rejects with error
363
*/
364
function unwrapResult<T>(action: { payload: T } | { error: SerializedError | any }): T;
365
366
/**
367
* Serializes Error objects to plain objects
368
* @param value - Error or other value to serialize
369
* @returns Serialized error object
370
*/
371
function miniSerializeError(value: any): SerializedError;
372
373
interface SerializedError {
374
name?: string;
375
message?: string;
376
code?: string;
377
stack?: string;
378
}
379
```
380
381
**Usage Examples:**
382
383
```typescript
384
import { unwrapResult } from '@reduxjs/toolkit';
385
386
// Component usage with unwrapResult
387
const UserProfile = () => {
388
const dispatch = useAppDispatch();
389
const [loading, setLoading] = useState(false);
390
391
const handleFetchUser = async (userId: string) => {
392
setLoading(true);
393
try {
394
// unwrapResult will throw if the thunk was rejected
395
const user = await dispatch(fetchUserById(userId)).then(unwrapResult);
396
console.log('User loaded:', user);
397
// Handle success
398
} catch (error) {
399
console.error('Failed to load user:', error);
400
// Handle error
401
} finally {
402
setLoading(false);
403
}
404
};
405
406
// ... component JSX
407
};
408
409
// Using with async/await in thunks
410
const complexOperation = createAsyncThunk(
411
'complex/operation',
412
async (data, { dispatch }) => {
413
try {
414
// Chain multiple async thunks
415
const user = await dispatch(fetchUserById(data.userId)).then(unwrapResult);
416
const settings = await dispatch(fetchUserSettings(user.id)).then(unwrapResult);
417
418
return { user, settings };
419
} catch (error) {
420
// Handle any of the chained operations failing
421
throw error; // Will be caught by thunk and trigger rejected action
422
}
423
}
424
);
425
426
// Custom error serialization
427
const customErrorThunk = createAsyncThunk(
428
'data/fetchWithCustomError',
429
async (id: string) => {
430
throw new CustomError('Something went wrong', 'CUSTOM_ERROR_CODE');
431
},
432
{
433
serializeError: (err: any) => ({
434
...miniSerializeError(err),
435
customCode: err.code,
436
timestamp: Date.now()
437
})
438
}
439
);
440
```
441
442
## Action Matchers
443
444
Redux Toolkit provides action matchers for handling async thunk actions in reducers.
445
446
```typescript { .api }
447
/**
448
* Matches pending actions from async thunks
449
* @param asyncThunks - Async thunk action creators to match
450
* @returns Action matcher function
451
*/
452
function isPending(...asyncThunks: AsyncThunk<any, any, any>[]): ActionMatcher<PendingAction<any>>;
453
454
/**
455
* Matches fulfilled actions from async thunks
456
* @param asyncThunks - Async thunk action creators to match
457
* @returns Action matcher function
458
*/
459
function isFulfilled(...asyncThunks: AsyncThunk<any, any, any>[]): ActionMatcher<FulfilledAction<any, any>>;
460
461
/**
462
* Matches rejected actions from async thunks
463
* @param asyncThunks - Async thunk action creators to match
464
* @returns Action matcher function
465
*/
466
function isRejected(...asyncThunks: AsyncThunk<any, any, any>[]): ActionMatcher<RejectedAction<any, any>>;
467
468
/**
469
* Matches any action from async thunks (pending, fulfilled, or rejected)
470
* @param asyncThunks - Async thunk action creators to match
471
* @returns Action matcher function
472
*/
473
function isAsyncThunkAction(...asyncThunks: AsyncThunk<any, any, any>[]): ActionMatcher<AnyAsyncThunkAction>;
474
475
/**
476
* Matches rejected actions that used rejectWithValue
477
* @param asyncThunks - Async thunk action creators to match
478
* @returns Action matcher function
479
*/
480
function isRejectedWithValue(...asyncThunks: AsyncThunk<any, any, any>[]): ActionMatcher<RejectedWithValueAction<any, any>>;
481
```
482
483
**Usage Examples:**
484
485
```typescript
486
import {
487
isPending,
488
isFulfilled,
489
isRejected,
490
isAsyncThunkAction,
491
isRejectedWithValue
492
} from '@reduxjs/toolkit';
493
494
const dataSlice = createSlice({
495
name: 'data',
496
initialState: {
497
loading: false,
498
error: null,
499
users: {},
500
posts: {}
501
},
502
reducers: {},
503
extraReducers: (builder) => {
504
builder
505
// Handle all pending states
506
.addMatcher(
507
isPending(fetchUserById, fetchPosts, updateUser),
508
(state) => {
509
state.loading = true;
510
state.error = null;
511
}
512
)
513
// Handle all fulfilled states
514
.addMatcher(
515
isFulfilled(fetchUserById, fetchPosts, updateUser),
516
(state) => {
517
state.loading = false;
518
}
519
)
520
// Handle rejected states with custom values
521
.addMatcher(
522
isRejectedWithValue(fetchUserById, updateUser),
523
(state, action) => {
524
state.loading = false;
525
state.error = action.payload; // Custom error from rejectWithValue
526
}
527
)
528
// Handle other rejected states
529
.addMatcher(
530
isRejected(fetchUserById, fetchPosts, updateUser),
531
(state, action) => {
532
state.loading = false;
533
state.error = action.error.message || 'Something went wrong';
534
}
535
);
536
}
537
});
538
539
// Generic loading handler for multiple thunks
540
const createLoadingSlice = (thunks: AsyncThunk<any, any, any>[]) =>
541
createSlice({
542
name: 'loading',
543
initialState: { isLoading: false },
544
reducers: {},
545
extraReducers: (builder) => {
546
builder
547
.addMatcher(isPending(...thunks), (state) => {
548
state.isLoading = true;
549
})
550
.addMatcher(isFulfilled(...thunks), (state) => {
551
state.isLoading = false;
552
})
553
.addMatcher(isRejected(...thunks), (state) => {
554
state.isLoading = false;
555
});
556
}
557
});
558
```
559
560
## Advanced Patterns
561
562
### Request Cancellation
563
564
```typescript
565
// Thunk with cancellation support
566
const cancellableThunk = createAsyncThunk(
567
'data/fetch',
568
async (params, { signal }) => {
569
const response = await fetch('/api/data', { signal });
570
return response.json();
571
}
572
);
573
574
// Component with cancellation
575
const DataComponent = () => {
576
const dispatch = useAppDispatch();
577
const promiseRef = useRef<any>();
578
579
useEffect(() => {
580
promiseRef.current = dispatch(cancellableThunk(params));
581
582
return () => {
583
promiseRef.current?.abort();
584
};
585
}, [dispatch, params]);
586
};
587
```
588
589
### Thunk Chaining
590
591
```typescript
592
const chainedOperation = createAsyncThunk(
593
'data/chainedOperation',
594
async (userId: string, { dispatch, getState }) => {
595
// Chain multiple async operations
596
const user = await dispatch(fetchUserById(userId)).then(unwrapResult);
597
const profile = await dispatch(fetchUserProfile(user.id)).then(unwrapResult);
598
const permissions = await dispatch(fetchUserPermissions(user.id)).then(unwrapResult);
599
600
return { user, profile, permissions };
601
}
602
);
603
```
604
605
### Optimistic Updates
606
607
```typescript
608
const updateUserOptimistic = createAsyncThunk(
609
'users/updateOptimistic',
610
async (
611
{ id, updates }: { id: string; updates: Partial<User> },
612
{ dispatch, rejectWithValue }
613
) => {
614
// Optimistically apply update
615
dispatch(userUpdatedOptimistically({ id, updates }));
616
617
try {
618
const updatedUser = await userAPI.update(id, updates);
619
return { id, user: updatedUser };
620
} catch (error: any) {
621
// Revert optimistic update on failure
622
dispatch(revertOptimisticUpdate(id));
623
return rejectWithValue(error.message);
624
}
625
}
626
);
627
```