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
```