or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

actions-reducers.mdasync-thunks.mdcore-store.mdentity-adapters.mdindex.mdmiddleware.mdreact-integration.mdrtk-query-react.mdrtk-query.mdutilities.md

async-thunks.mddocs/

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

```