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

middleware.mddocs/

0

# Middleware

1

2

Redux Toolkit provides advanced middleware for side effects, dynamic middleware injection, and development-time invariants.

3

4

## Capabilities

5

6

### Listener Middleware

7

8

Creates middleware for running side effects in response to actions or state changes with precise lifecycle control.

9

10

```typescript { .api }

11

/**

12

* Creates middleware for running side effects in response to actions

13

* @param options - Configuration options for the listener middleware

14

* @returns ListenerMiddlewareInstance with middleware and control methods

15

*/

16

function createListenerMiddleware<

17

StateType = unknown,

18

DispatchType extends Dispatch = AppDispatch,

19

ExtraArgument = unknown

20

>(options?: CreateListenerMiddlewareOptions<ExtraArgument>): ListenerMiddlewareInstance<StateType, DispatchType, ExtraArgument>;

21

22

interface CreateListenerMiddlewareOptions<ExtraArgument> {

23

/** Additional context passed to listeners */

24

extra?: ExtraArgument;

25

/** Error handler for listener exceptions */

26

onError?: ListenerErrorHandler;

27

}

28

29

type ListenerErrorHandler = (error: unknown, errorInfo: ListenerErrorInfo) => void;

30

31

interface ListenerErrorInfo {

32

runtimeInfo: {

33

listenerApi: ListenerApi<any, any, any>;

34

extraArgument: any;

35

};

36

}

37

38

interface ListenerMiddlewareInstance<StateType, DispatchType, ExtraArgument> {

39

/** The actual middleware function for store configuration */

40

middleware: Middleware<{}, StateType, DispatchType>;

41

42

/** Add a listener for actions or state changes */

43

startListening<ActionCreator extends TypedActionCreator<any>>(

44

options: ListenerOptions<ActionCreator, StateType, DispatchType, ExtraArgument>

45

): UnsubscribeListener;

46

47

/** Remove a specific listener */

48

stopListening<ActionCreator extends TypedActionCreator<any>>(

49

options: ListenerOptions<ActionCreator, StateType, DispatchType, ExtraArgument>

50

): boolean;

51

52

/** Remove all listeners */

53

clearListeners(): void;

54

}

55

56

interface ListenerOptions<ActionCreator, StateType, DispatchType, ExtraArgument> {

57

/** Action creator or action type to listen for */

58

actionCreator?: ActionCreator;

59

type?: string;

60

matcher?: ActionMatcher<any>;

61

predicate?: ListenerPredicate<StateType, DispatchType>;

62

63

/** The effect function to run */

64

effect: ListenerEffect<ActionCreator, StateType, DispatchType, ExtraArgument>;

65

}

66

67

type ListenerEffect<ActionCreator, StateType, DispatchType, ExtraArgument> = (

68

action: ActionCreator extends TypedActionCreator<infer Action> ? Action : AnyAction,

69

listenerApi: ListenerApi<StateType, DispatchType, ExtraArgument>

70

) => void | Promise<void>;

71

72

interface ListenerApi<StateType, DispatchType, ExtraArgument> {

73

/** Get current state */

74

getState(): StateType;

75

/** Get original state when action was dispatched */

76

getOriginalState(): StateType;

77

/** Dispatch actions */

78

dispatch: DispatchType;

79

/** Extra argument from middleware options */

80

extra: ExtraArgument;

81

/** Unique request ID for this listener execution */

82

requestId: string;

83

/** Take future actions that match conditions */

84

take: TakePattern<StateType, DispatchType>;

85

/** Cancel the current listener */

86

cancelActiveListeners(): void;

87

/** AbortController signal for cancellation */

88

signal: AbortSignal;

89

/** Fork async tasks */

90

fork(executor: (forkApi: ForkApi<StateType, DispatchType, ExtraArgument>) => void | Promise<void>): TaskAbortError;

91

/** Delay execution */

92

delay(ms: number): Promise<void>;

93

/** Pause until condition is met */

94

condition(predicate: ListenerPredicate<StateType, DispatchType>, timeout?: number): Promise<boolean>;

95

/** Unsubscribe this listener */

96

unsubscribe(): void;

97

/** Subscribe to actions without removing listener */

98

subscribe(): void;

99

}

100

```

101

102

**Usage Examples:**

103

104

```typescript

105

import { createListenerMiddleware, isAnyOf } from '@reduxjs/toolkit';

106

107

// Create listener middleware

108

const listenerMiddleware = createListenerMiddleware({

109

extra: { api, analytics }

110

});

111

112

// Simple action listener

113

listenerMiddleware.startListening({

114

actionCreator: userLoggedIn,

115

effect: async (action, listenerApi) => {

116

const { dispatch, extra } = listenerApi;

117

118

// Side effect: track login

119

extra.analytics.track('user_login', {

120

userId: action.payload.id,

121

timestamp: Date.now()

122

});

123

124

// Load user preferences

125

dispatch(loadUserPreferences(action.payload.id));

126

}

127

});

128

129

// Multiple action types

130

listenerMiddleware.startListening({

131

matcher: isAnyOf(userLoggedIn, userRegistered),

132

effect: (action, listenerApi) => {

133

// Handle both login and registration

134

listenerApi.extra.analytics.track('user_authenticated');

135

}

136

});

137

138

// State-based listener with predicate

139

listenerMiddleware.startListening({

140

predicate: (action, currentState, previousState) => {

141

return currentState.user.isAuthenticated && !previousState.user.isAuthenticated;

142

},

143

effect: async (action, listenerApi) => {

144

// User became authenticated

145

await listenerApi.extra.api.setupUserSession();

146

}

147

});

148

149

// Complex async workflow

150

listenerMiddleware.startListening({

151

actionCreator: startDataSync,

152

effect: async (action, listenerApi) => {

153

const { fork, delay, take, condition, signal } = listenerApi;

154

155

// Fork background sync process

156

const syncTask = fork(async (forkApi) => {

157

while (!signal.aborted) {

158

try {

159

await forkApi.extra.api.syncData();

160

await forkApi.delay(30000); // Sync every 30 seconds

161

} catch (error) {

162

forkApi.dispatch(syncErrorOccurred(error));

163

break;

164

}

165

}

166

});

167

168

// Wait for stop signal

169

await take(stopDataSync);

170

171

// Cancel the sync task

172

syncTask.cancel();

173

}

174

});

175

176

// Add to store

177

const store = configureStore({

178

reducer: rootReducer,

179

middleware: (getDefaultMiddleware) =>

180

getDefaultMiddleware().prepend(listenerMiddleware.middleware)

181

});

182

```

183

184

### Dynamic Middleware

185

186

Creates middleware that allows adding and removing other middleware at runtime.

187

188

```typescript { .api }

189

/**

190

* Creates middleware that can have other middleware added at runtime

191

* @returns DynamicMiddlewareInstance with middleware and control methods

192

*/

193

function createDynamicMiddleware<

194

State = any,

195

DispatchType extends Dispatch<AnyAction> = Dispatch<AnyAction>

196

>(): DynamicMiddlewareInstance<State, DispatchType>;

197

198

interface DynamicMiddlewareInstance<State, DispatchType> {

199

/** The actual middleware function for store configuration */

200

middleware: Middleware<{}, State, DispatchType>;

201

202

/** Add middleware programmatically */

203

addMiddleware(...middlewares: Middleware<any, State, DispatchType>[]): void;

204

205

/** Action creator to add middleware via dispatch */

206

withMiddleware(...middlewares: Middleware<any, State, DispatchType>[]): PayloadAction<Middleware<any, State, DispatchType>[]>;

207

208

/** Unique instance identifier */

209

instanceId: string;

210

}

211

```

212

213

**Usage Examples:**

214

215

```typescript

216

import { createDynamicMiddleware } from '@reduxjs/toolkit';

217

218

// Create dynamic middleware

219

const dynamicMiddleware = createDynamicMiddleware();

220

221

const store = configureStore({

222

reducer: rootReducer,

223

middleware: (getDefaultMiddleware) =>

224

getDefaultMiddleware().concat(dynamicMiddleware.middleware)

225

});

226

227

// Add middleware programmatically

228

const analyticsMiddleware: Middleware = (store) => (next) => (action) => {

229

// Track actions

230

analytics.track(action.type, action.payload);

231

return next(action);

232

};

233

234

dynamicMiddleware.addMiddleware(analyticsMiddleware);

235

236

// Add middleware via action

237

store.dispatch(

238

dynamicMiddleware.withMiddleware(

239

loggerMiddleware,

240

crashReportingMiddleware

241

)

242

);

243

244

// Conditional middleware loading

245

if (process.env.NODE_ENV === 'development') {

246

dynamicMiddleware.addMiddleware(devOnlyMiddleware);

247

}

248

249

// Feature-based middleware

250

const featureSlice = createSlice({

251

name: 'feature',

252

initialState: { enabled: false },

253

reducers: {

254

featureEnabled: (state, action) => {

255

state.enabled = true;

256

// Add feature-specific middleware when enabled

257

}

258

},

259

extraReducers: (builder) => {

260

builder.addCase(featureEnabled, (state, action) => {

261

// This would be handled in a listener or component

262

});

263

}

264

});

265

```

266

267

### React Dynamic Middleware

268

269

Enhanced dynamic middleware with React hook factories for component-specific middleware.

270

271

```typescript { .api }

272

/**

273

* React-enhanced version of dynamic middleware with hook factories

274

* Available in @reduxjs/toolkit/react

275

*/

276

interface ReactDynamicMiddlewareInstance<State, DispatchType> extends DynamicMiddlewareInstance<State, DispatchType> {

277

/** Creates hook factory for specific React context */

278

createDispatchWithMiddlewareHookFactory(context?: React.Context<any>): CreateDispatchWithMiddlewareHook<State, DispatchType>;

279

280

/** Default hook factory */

281

createDispatchWithMiddlewareHook: CreateDispatchWithMiddlewareHook<State, DispatchType>;

282

}

283

284

interface CreateDispatchWithMiddlewareHook<State, DispatchType> {

285

(middlewares: Middleware<any, State, DispatchType>[]): DispatchType;

286

}

287

```

288

289

**Usage Examples:**

290

291

```typescript

292

// Available from @reduxjs/toolkit/react

293

import { createDynamicMiddleware } from '@reduxjs/toolkit/react';

294

295

const dynamicMiddleware = createDynamicMiddleware<RootState, AppDispatch>();

296

297

// In components

298

const MyComponent = () => {

299

const dispatchWithAnalytics = dynamicMiddleware.createDispatchWithMiddlewareHook([

300

analyticsMiddleware,

301

loggingMiddleware

302

]);

303

304

const handleAction = () => {

305

// This dispatch will go through the additional middleware

306

dispatchWithAnalytics(someAction());

307

};

308

309

return <button onClick={handleAction}>Action</button>;

310

};

311

312

// Component-specific middleware factory

313

const useComponentMiddleware = dynamicMiddleware.createDispatchWithMiddlewareHookFactory();

314

315

const FeatureComponent = () => {

316

const dispatch = useComponentMiddleware([

317

featureSpecificMiddleware,

318

performanceTrackingMiddleware

319

]);

320

321

// Use dispatch with component-specific middleware

322

};

323

```

324

325

### Development Middleware

326

327

Middleware for development-time checking and debugging.

328

329

```typescript { .api }

330

/**

331

* Middleware that detects mutations to state objects

332

* @param options - Configuration options

333

* @returns Middleware function that throws on mutations

334

*/

335

function createImmutableStateInvariantMiddleware<S = any>(

336

options?: ImmutableStateInvariantMiddlewareOptions

337

): Middleware<{}, S>;

338

339

interface ImmutableStateInvariantMiddlewareOptions {

340

/** Function to check if value should be considered immutable */

341

isImmutable?: (value: any) => boolean;

342

/** State paths to ignore during checking */

343

ignoredPaths?: string[];

344

/** Execution time warning threshold in ms */

345

warnAfter?: number;

346

}

347

348

/**

349

* Middleware that detects non-serializable values in actions and state

350

* @param options - Configuration options

351

* @returns Middleware function that warns about non-serializable values

352

*/

353

function createSerializableStateInvariantMiddleware(

354

options?: SerializableStateInvariantMiddlewareOptions

355

): Middleware<{}, any>;

356

357

interface SerializableStateInvariantMiddlewareOptions {

358

/** Action types to ignore */

359

ignoredActions?: string[];

360

/** Action property paths to ignore */

361

ignoredActionPaths?: string[];

362

/** State paths to ignore */

363

ignoredPaths?: string[];

364

/** Function to check if value is serializable */

365

isSerializable?: (value: any) => boolean;

366

/** Function to get object entries for checking */

367

getEntries?: (value: any) => [string, any][];

368

/** Execution time warning threshold in ms */

369

warnAfter?: number;

370

}

371

372

/**

373

* Middleware that validates action creators are called correctly

374

* @param options - Configuration options

375

* @returns Middleware function that warns about misused action creators

376

*/

377

function createActionCreatorInvariantMiddleware(

378

options?: ActionCreatorInvariantMiddlewareOptions

379

): Middleware;

380

381

interface ActionCreatorInvariantMiddlewareOptions {

382

/** Function to identify action creators */

383

isActionCreator?: (value: any) => boolean;

384

}

385

```

386

387

**Usage Examples:**

388

389

```typescript

390

import {

391

createImmutableStateInvariantMiddleware,

392

createSerializableStateInvariantMiddleware,

393

createActionCreatorInvariantMiddleware

394

} from '@reduxjs/toolkit';

395

396

// Custom immutability checking

397

const immutableCheckMiddleware = createImmutableStateInvariantMiddleware({

398

ignoredPaths: ['router.location', 'form.values'],

399

warnAfter: 32, // Warn if checking takes more than 32ms

400

isImmutable: (value) => {

401

// Custom immutability logic

402

return typeof value !== 'object' || value === null || Object.isFrozen(value);

403

}

404

});

405

406

// Custom serializability checking

407

const serializableCheckMiddleware = createSerializableStateInvariantMiddleware({

408

ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE'],

409

ignoredPaths: ['auth.token', 'ui.fileUpload'],

410

ignoredActionPaths: ['payload.file', 'meta.timestamp'],

411

warnAfter: 64,

412

isSerializable: (value) => {

413

// Allow specific non-serializable types

414

return typeof value !== 'function' && !(value instanceof File);

415

}

416

});

417

418

// Action creator checking

419

const actionCreatorCheckMiddleware = createActionCreatorInvariantMiddleware({

420

isActionCreator: (value) => {

421

return typeof value === 'function' &&

422

typeof value.type === 'string' &&

423

typeof value.match === 'function';

424

}

425

});

426

427

const store = configureStore({

428

reducer: rootReducer,

429

middleware: (getDefaultMiddleware) =>

430

getDefaultMiddleware()

431

.concat(

432

immutableCheckMiddleware,

433

serializableCheckMiddleware,

434

actionCreatorCheckMiddleware

435

)

436

});

437

```

438

439

### Serialization Utilities

440

441

Utility functions for checking serialization and finding non-serializable values.

442

443

```typescript { .api }

444

/**

445

* Checks if a value is "plain" (JSON-serializable)

446

* @param val - Value to check

447

* @returns True if value is directly serializable

448

*/

449

function isPlain(val: any): boolean;

450

451

/**

452

* Finds non-serializable values in nested objects

453

* @param value - Value to examine

454

* @param path - Current path in object (for error reporting)

455

* @param isSerializable - Function to check if value is serializable

456

* @param getEntries - Function to get object entries

457

* @param ignoredPaths - Paths to ignore during checking

458

* @param cache - WeakSet cache to avoid circular references

459

* @returns Non-serializable value info or false if all values are serializable

460

*/

461

function findNonSerializableValue(

462

value: unknown,

463

path?: string,

464

isSerializable?: (value: unknown) => boolean,

465

getEntries?: (value: unknown) => [string, any][],

466

ignoredPaths?: string[],

467

cache?: WeakSet<object>

468

): { keyPath: string; value: unknown } | false;

469

470

/**

471

* Checks if value is immutable (for development middleware)

472

* @param value - Value to check

473

* @returns True if value should be treated as immutable

474

*/

475

function isImmutableDefault(value: any): boolean;

476

```

477

478

**Usage Examples:**

479

480

```typescript

481

import {

482

isPlain,

483

findNonSerializableValue,

484

isImmutableDefault

485

} from '@reduxjs/toolkit';

486

487

// Check if values are serializable

488

console.log(isPlain({ name: 'John', age: 30 })); // true

489

console.log(isPlain(new Date())); // false

490

console.log(isPlain(function() {})); // false

491

492

// Find non-serializable values

493

const state = {

494

user: { name: 'John', age: 30 },

495

callback: () => console.log('hello'), // Non-serializable

496

timestamp: new Date() // Non-serializable

497

};

498

499

const nonSerializable = findNonSerializableValue(state);

500

if (nonSerializable) {

501

console.log(`Non-serializable value at ${nonSerializable.keyPath}:`, nonSerializable.value);

502

// Output: "Non-serializable value at callback: [Function]"

503

}

504

505

// Custom serialization check

506

const customCheck = findNonSerializableValue(

507

state,

508

'',

509

(value) => isPlain(value) || value instanceof Date // Allow Dates

510

);

511

512

// Check immutability

513

console.log(isImmutableDefault({})); // false (plain objects are mutable)

514

console.log(isImmutableDefault([])); // false (arrays are mutable)

515

console.log(isImmutableDefault('string')); // true (strings are immutable)

516

```

517

518

### Auto-batching

519

520

Enhancer and utilities for automatic action batching to optimize React renders.

521

522

```typescript { .api }

523

/**

524

* Store enhancer for automatic action batching

525

* @param options - Batching configuration options

526

* @returns Store enhancer function

527

*/

528

function autoBatchEnhancer(options?: AutoBatchOptions): StoreEnhancer;

529

530

interface AutoBatchOptions {

531

/** Batching strategy */

532

type?: 'tick' | 'timer' | 'callback' | ((action: Action) => boolean);

533

}

534

535

/** Symbol to mark actions for auto-batching */

536

const SHOULD_AUTOBATCH: unique symbol;

537

538

/**

539

* Prepare function for auto-batched actions

540

* @param payload - Action payload

541

* @returns Prepared action with auto-batch symbol

542

*/

543

function prepareAutoBatched<T>(payload: T): {

544

payload: T;

545

meta: { [SHOULD_AUTOBATCH]: true };

546

};

547

```

548

549

**Usage Examples:**

550

551

```typescript

552

import { autoBatchEnhancer, prepareAutoBatched, SHOULD_AUTOBATCH } from '@reduxjs/toolkit';

553

554

// Configure store with auto-batching

555

const store = configureStore({

556

reducer: rootReducer,

557

enhancers: (getDefaultEnhancers) =>

558

getDefaultEnhancers().concat(

559

autoBatchEnhancer({

560

type: 'tick' // Batch actions until next tick

561

})

562

)

563

});

564

565

// Mark actions for batching

566

const userSlice = createSlice({

567

name: 'user',

568

initialState: { users: [], loading: false },

569

reducers: {

570

usersLoaded: {

571

reducer: (state, action) => {

572

state.users = action.payload;

573

state.loading = false;

574

},

575

prepare: prepareAutoBatched

576

},

577

578

// Alternative: manual batching metadata

579

bulkUpdateUsers: {

580

reducer: (state, action) => {

581

action.payload.forEach(update => {

582

const user = state.users.find(u => u.id === update.id);

583

if (user) {

584

Object.assign(user, update.changes);

585

}

586

});

587

},

588

prepare: (updates) => ({

589

payload: updates,

590

meta: { [SHOULD_AUTOBATCH]: true }

591

})

592

}

593

}

594

});

595

596

// Custom batching logic

597

const customBatchingStore = configureStore({

598

reducer: rootReducer,

599

enhancers: (getDefaultEnhancers) =>

600

getDefaultEnhancers().concat(

601

autoBatchEnhancer({

602

type: (action) => {

603

// Batch all async thunk fulfilled actions

604

return action.type.endsWith('/fulfilled');

605

}

606

})

607

)

608

});

609

```

610

611

## Advanced Patterns

612

613

### Listener Middleware Workflows

614

615

```typescript

616

// Complex workflow with multiple stages

617

listenerMiddleware.startListening({

618

actionCreator: startWorkflow,

619

effect: async (action, listenerApi) => {

620

const { fork, delay, take, condition, cancelActiveListeners } = listenerApi;

621

622

try {

623

// Stage 1: Initialize

624

listenerApi.dispatch(workflowStageChanged('initializing'));

625

await listenerApi.delay(1000);

626

627

// Stage 2: Process data

628

listenerApi.dispatch(workflowStageChanged('processing'));

629

const processTask = fork(async (forkApi) => {

630

for (let i = 0; i < 10; i++) {

631

await forkApi.delay(500);

632

forkApi.dispatch(workflowProgressUpdated(i * 10));

633

}

634

});

635

636

// Wait for completion or cancellation

637

const result = await Promise.race([

638

processTask.result,

639

take(cancelWorkflow).then(() => 'cancelled')

640

]);

641

642

if (result === 'cancelled') {

643

processTask.cancel();

644

listenerApi.dispatch(workflowCancelled());

645

} else {

646

listenerApi.dispatch(workflowCompleted());

647

}

648

} catch (error) {

649

listenerApi.dispatch(workflowFailed(error));

650

}

651

}

652

});

653

```

654

655

### Middleware Composition

656

657

```typescript

658

// Compose multiple middleware concerns

659

const createAppMiddleware = () => {

660

const listenerMiddleware = createListenerMiddleware();

661

const dynamicMiddleware = createDynamicMiddleware();

662

663

// Add core listeners

664

listenerMiddleware.startListening({

665

matcher: isAnyOf(userLoggedIn, userLoggedOut),

666

effect: (action, listenerApi) => {

667

// Authentication side effects

668

}

669

});

670

671

return {

672

listener: listenerMiddleware.middleware,

673

dynamic: dynamicMiddleware.middleware,

674

addListener: listenerMiddleware.startListening.bind(listenerMiddleware),

675

addMiddleware: dynamicMiddleware.addMiddleware.bind(dynamicMiddleware)

676

};

677

};

678

679

const appMiddleware = createAppMiddleware();

680

681

const store = configureStore({

682

reducer: rootReducer,

683

middleware: (getDefaultMiddleware) =>

684

getDefaultMiddleware()

685

.concat(appMiddleware.listener, appMiddleware.dynamic)

686

});

687

```

688

689

### Conditional Middleware Loading

690

691

```typescript

692

// Feature flag-based middleware loading

693

const createFeatureMiddleware = (features: FeatureFlags) => {

694

const middleware: Middleware[] = [];

695

696

if (features.analytics) {

697

middleware.push(analyticsMiddleware);

698

}

699

700

if (features.logging) {

701

middleware.push(loggingMiddleware);

702

}

703

704

if (features.performance) {

705

middleware.push(performanceMiddleware);

706

}

707

708

return middleware;

709

};

710

711

// Environment-based middleware

712

const environmentMiddleware = () => {

713

const middleware: Middleware[] = [];

714

715

if (process.env.NODE_ENV === 'development') {

716

middleware.push(

717

createImmutableStateInvariantMiddleware(),

718

createSerializableStateInvariantMiddleware()

719

);

720

}

721

722

if (process.env.ENABLE_REDUX_LOGGER) {

723

middleware.push(logger);

724

}

725

726

return middleware;

727

};

728

```