0
# Utilities
1
2
Redux Toolkit provides a comprehensive set of utility functions for selectors, action matching, state management, and development tools.
3
4
## Capabilities
5
6
### Selector Utilities
7
8
Redux Toolkit re-exports and enhances Reselect for creating memoized selectors with additional draft-safe functionality.
9
10
```typescript { .api }
11
/**
12
* Create a memoized selector from input selectors and result function
13
* Re-exported from Reselect with Redux Toolkit enhancements
14
*/
15
function createSelector<InputSelectors extends readonly unknown[], Result>(
16
...args: [...inputSelectors: InputSelectors, resultFunc: (...args: SelectorResultArray<InputSelectors>) => Result]
17
): Selector<GetStateFromSelectors<InputSelectors>, Result>;
18
19
/**
20
* Create custom selector creator with specific memoization function
21
*/
22
function createSelectorCreator<MemoizeFunction extends Function>(
23
memoizeFunc: MemoizeFunction,
24
...memoizeOptions: any[]
25
): <InputSelectors extends readonly unknown[], Result>(
26
...args: [...inputSelectors: InputSelectors, resultFunc: (...args: SelectorResultArray<InputSelectors>) => Result]
27
) => Selector<GetStateFromSelectors<InputSelectors>, Result>;
28
29
/**
30
* LRU (Least Recently Used) memoization function
31
* Re-exported from Reselect
32
*/
33
function lruMemoize<Func extends Function>(func: Func, equalityCheckOrOptions?: any): Func;
34
35
/**
36
* WeakMap-based memoization for object references
37
* Re-exported from Reselect
38
*/
39
function weakMapMemoize<Func extends Function>(func: Func): Func;
40
41
/**
42
* Create draft-safe selector that works with Immer draft objects
43
* RTK-specific enhancement
44
*/
45
function createDraftSafeSelector<InputSelectors extends readonly unknown[], Result>(
46
...args: [...inputSelectors: InputSelectors, resultFunc: (...args: SelectorResultArray<InputSelectors>) => Result]
47
): Selector<GetStateFromSelectors<InputSelectors>, Result>;
48
49
/**
50
* Create custom draft-safe selector creator
51
*/
52
function createDraftSafeSelectorCreator<MemoizeFunction extends Function>(
53
memoizeFunc: MemoizeFunction,
54
...memoizeOptions: any[]
55
): typeof createDraftSafeSelector;
56
```
57
58
**Usage Examples:**
59
60
```typescript
61
import {
62
createSelector,
63
createDraftSafeSelector,
64
lruMemoize,
65
weakMapMemoize
66
} from '@reduxjs/toolkit';
67
68
interface RootState {
69
todos: { id: string; text: string; completed: boolean }[];
70
filter: 'all' | 'active' | 'completed';
71
}
72
73
// Basic selector
74
const selectTodos = (state: RootState) => state.todos;
75
const selectFilter = (state: RootState) => state.filter;
76
77
// Memoized selector
78
const selectFilteredTodos = createSelector(
79
[selectTodos, selectFilter],
80
(todos, filter) => {
81
switch (filter) {
82
case 'active':
83
return todos.filter(todo => !todo.completed);
84
case 'completed':
85
return todos.filter(todo => todo.completed);
86
default:
87
return todos;
88
}
89
}
90
);
91
92
// Selector with arguments
93
const selectTodoById = createSelector(
94
[selectTodos, (state: RootState, id: string) => id],
95
(todos, id) => todos.find(todo => todo.id === id)
96
);
97
98
// Draft-safe selector for use with Immer drafts
99
const selectTodoStats = createDraftSafeSelector(
100
[selectTodos],
101
(todos) => ({
102
total: todos.length,
103
completed: todos.filter(todo => todo.completed).length,
104
active: todos.filter(todo => !todo.completed).length
105
})
106
);
107
108
// Custom memoization
109
const selectExpensiveComputation = createSelector(
110
[selectTodos],
111
(todos) => {
112
// Expensive computation
113
return todos.map(todo => ({
114
...todo,
115
hash: computeExpensiveHash(todo)
116
}));
117
},
118
{
119
// Use LRU memoization with cache size 10
120
memoize: lruMemoize,
121
memoizeOptions: { maxSize: 10 }
122
}
123
);
124
125
// WeakMap memoization for object keys
126
const createObjectKeySelector = createSelectorCreator(weakMapMemoize);
127
128
const selectTodosByCategory = createObjectKeySelector(
129
[selectTodos, (state: RootState, category: object) => category],
130
(todos, category) => todos.filter(todo => todo.category === category)
131
);
132
133
// Usage in components
134
const TodoList = () => {
135
const filteredTodos = useAppSelector(selectFilteredTodos);
136
const stats = useAppSelector(selectTodoStats);
137
138
return (
139
<div>
140
<div>Total: {stats.total}, Active: {stats.active}, Completed: {stats.completed}</div>
141
{filteredTodos.map(todo => <TodoItem key={todo.id} todo={todo} />)}
142
</div>
143
);
144
};
145
146
const TodoDetail = ({ todoId }: { todoId: string }) => {
147
const todo = useAppSelector(state => selectTodoById(state, todoId));
148
149
return todo ? <div>{todo.text}</div> : <div>Todo not found</div>;
150
};
151
```
152
153
### Action Matching Utilities
154
155
Powerful utilities for matching and combining action matchers in reducers and middleware.
156
157
```typescript { .api }
158
/**
159
* Creates matcher that requires all conditions to be true
160
* @param matchers - Array of action matchers or action creators
161
* @returns Combined action matcher
162
*/
163
function isAllOf<A extends AnyAction>(
164
...matchers: readonly ActionMatcherOrType<A>[]
165
): ActionMatcher<A>;
166
167
/**
168
* Creates matcher that requires any condition to be true
169
* @param matchers - Array of action matchers or action creators
170
* @returns Combined action matcher
171
*/
172
function isAnyOf<A extends AnyAction>(
173
...matchers: readonly ActionMatcherOrType<A>[]
174
): ActionMatcher<A>;
175
176
/**
177
* Matches pending actions from async thunks
178
* @param asyncThunks - Async thunk action creators to match
179
* @returns Action matcher for pending states
180
*/
181
function isPending(...asyncThunks: AsyncThunk<any, any, any>[]): ActionMatcher<PendingAction<any>>;
182
183
/**
184
* Matches fulfilled actions from async thunks
185
* @param asyncThunks - Async thunk action creators to match
186
* @returns Action matcher for fulfilled states
187
*/
188
function isFulfilled(...asyncThunks: AsyncThunk<any, any, any>[]): ActionMatcher<FulfilledAction<any, any>>;
189
190
/**
191
* Matches rejected actions from async thunks
192
* @param asyncThunks - Async thunk action creators to match
193
* @returns Action matcher for rejected states
194
*/
195
function isRejected(...asyncThunks: AsyncThunk<any, any, any>[]): ActionMatcher<RejectedAction<any, any>>;
196
197
/**
198
* Matches any action from async thunks (pending, fulfilled, or rejected)
199
* @param asyncThunks - Async thunk action creators to match
200
* @returns Action matcher for any async thunk action
201
*/
202
function isAsyncThunkAction(...asyncThunks: AsyncThunk<any, any, any>[]): ActionMatcher<AnyAsyncThunkAction>;
203
204
/**
205
* Matches rejected actions that used rejectWithValue
206
* @param asyncThunks - Async thunk action creators to match
207
* @returns Action matcher for rejected with value actions
208
*/
209
function isRejectedWithValue(...asyncThunks: AsyncThunk<any, any, any>[]): ActionMatcher<RejectedWithValueAction<any, any>>;
210
211
type ActionMatcher<A extends AnyAction> = (action: AnyAction) => action is A;
212
type ActionMatcherOrType<A extends AnyAction> = ActionMatcher<A> | string | ActionCreator<A>;
213
```
214
215
**Usage Examples:**
216
217
```typescript
218
import {
219
isAllOf,
220
isAnyOf,
221
isPending,
222
isFulfilled,
223
isRejected,
224
isRejectedWithValue
225
} from '@reduxjs/toolkit';
226
227
// Action creators and async thunks
228
const increment = createAction('counter/increment');
229
const decrement = createAction('counter/decrement');
230
const reset = createAction('counter/reset');
231
232
const fetchUser = createAsyncThunk('user/fetch', async (id: string) => {
233
const response = await api.getUser(id);
234
return response.data;
235
});
236
237
const updateUser = createAsyncThunk('user/update', async (data: UserData) => {
238
const response = await api.updateUser(data);
239
return response.data;
240
});
241
242
// Complex matchers in reducers
243
const appSlice = createSlice({
244
name: 'app',
245
initialState: {
246
counter: 0,
247
loading: false,
248
error: null as string | null,
249
lastAction: null as string | null
250
},
251
reducers: {},
252
extraReducers: (builder) => {
253
builder
254
// Match any counter action
255
.addMatcher(
256
isAnyOf(increment, decrement, reset),
257
(state, action) => {
258
state.lastAction = action.type;
259
state.error = null;
260
}
261
)
262
// Match all counter increment actions (example with multiple conditions)
263
.addMatcher(
264
isAllOf(
265
(action): action is PayloadAction<number> =>
266
typeof action.payload === 'number',
267
isAnyOf(increment)
268
),
269
(state, action) => {
270
state.counter += action.payload;
271
}
272
)
273
// Match any pending async action
274
.addMatcher(
275
isPending(fetchUser, updateUser),
276
(state) => {
277
state.loading = true;
278
state.error = null;
279
}
280
)
281
// Match any fulfilled async action
282
.addMatcher(
283
isFulfilled(fetchUser, updateUser),
284
(state) => {
285
state.loading = false;
286
}
287
)
288
// Match rejected actions with custom error values
289
.addMatcher(
290
isRejectedWithValue(fetchUser, updateUser),
291
(state, action) => {
292
state.loading = false;
293
state.error = action.payload as string;
294
}
295
)
296
// Match other rejected actions
297
.addMatcher(
298
isRejected(fetchUser, updateUser),
299
(state, action) => {
300
state.loading = false;
301
state.error = action.error.message || 'Unknown error';
302
}
303
);
304
}
305
});
306
307
// Custom matchers
308
const isUserAction = (action: AnyAction): action is AnyAction => {
309
return action.type.startsWith('user/');
310
};
311
312
const isHighPriorityAction = (action: AnyAction): action is AnyAction => {
313
return action.meta?.priority === 'high';
314
};
315
316
// Combine custom matchers
317
const isHighPriorityUserAction = isAllOf(isUserAction, isHighPriorityAction);
318
319
const prioritySlice = createSlice({
320
name: 'priority',
321
initialState: { highPriorityUserActions: 0 },
322
reducers: {},
323
extraReducers: (builder) => {
324
builder.addMatcher(
325
isHighPriorityUserAction,
326
(state) => {
327
state.highPriorityUserActions += 1;
328
}
329
);
330
}
331
});
332
333
// Middleware usage
334
const actionMatchingMiddleware: Middleware = (store) => (next) => (action) => {
335
if (isAnyOf(increment, decrement)(action)) {
336
console.log('Counter action dispatched:', action);
337
}
338
339
if (isPending(fetchUser, updateUser)(action)) {
340
console.log('Async operation started:', action);
341
}
342
343
return next(action);
344
};
345
```
346
347
### State Utilities
348
349
Utilities for advanced state management patterns and slice composition.
350
351
```typescript { .api }
352
/**
353
* Combines multiple slices into a single reducer with injection support
354
* @param slices - Slice objects to combine
355
* @returns Combined reducer with slice injection capabilities
356
*/
357
function combineSlices(...slices: Slice[]): CombinedSliceReducer;
358
359
interface CombinedSliceReducer extends Reducer {
360
/** Inject additional slices at runtime */
361
inject<S extends Slice>(slice: S, config?: { reducerPath?: string }): CombinedSliceReducer;
362
/** Get the current slice configuration */
363
selector: (state: any) => any;
364
}
365
366
/**
367
* Symbol to mark actions for auto-batching
368
*/
369
const SHOULD_AUTOBATCH: unique symbol;
370
371
/**
372
* Prepare function for auto-batched actions
373
* @param payload - Action payload
374
* @returns Prepared action with auto-batch metadata
375
*/
376
function prepareAutoBatched<T>(payload: T): {
377
payload: T;
378
meta: { [SHOULD_AUTOBATCH]: true };
379
};
380
381
/**
382
* Store enhancer for automatic action batching
383
* @param options - Batching configuration
384
* @returns Store enhancer function
385
*/
386
function autoBatchEnhancer(options?: {
387
type?: 'tick' | 'timer' | 'callback' | ((action: Action) => boolean);
388
}): StoreEnhancer;
389
```
390
391
**Usage Examples:**
392
393
```typescript
394
import { combineSlices, prepareAutoBatched, autoBatchEnhancer } from '@reduxjs/toolkit';
395
396
// Individual slices
397
const counterSlice = createSlice({
398
name: 'counter',
399
initialState: { value: 0 },
400
reducers: {
401
increment: (state) => { state.value += 1; },
402
decrement: (state) => { state.value -= 1; }
403
}
404
});
405
406
const todosSlice = createSlice({
407
name: 'todos',
408
initialState: [] as Todo[],
409
reducers: {
410
addTodo: (state, action) => {
411
state.push(action.payload);
412
}
413
}
414
});
415
416
// Combine slices
417
const rootReducer = combineSlices(counterSlice, todosSlice);
418
419
// Add slice at runtime
420
const userSlice = createSlice({
421
name: 'user',
422
initialState: { profile: null },
423
reducers: {
424
setProfile: (state, action) => {
425
state.profile = action.payload;
426
}
427
}
428
});
429
430
// Inject new slice
431
const enhancedReducer = rootReducer.inject(userSlice);
432
433
// Store with combined slices
434
const store = configureStore({
435
reducer: enhancedReducer,
436
enhancers: (getDefaultEnhancers) =>
437
getDefaultEnhancers().concat(
438
autoBatchEnhancer({ type: 'tick' })
439
)
440
});
441
442
// Auto-batched actions
443
const batchedSlice = createSlice({
444
name: 'batched',
445
initialState: { items: [], count: 0 },
446
reducers: {
447
bulkAddItems: {
448
reducer: (state, action) => {
449
state.items.push(...action.payload);
450
state.count = state.items.length;
451
},
452
prepare: prepareAutoBatched
453
},
454
batchedUpdate: {
455
reducer: (state, action) => {
456
Object.assign(state, action.payload);
457
},
458
prepare: (updates: any) => ({
459
payload: updates,
460
meta: { [SHOULD_AUTOBATCH]: true }
461
})
462
}
463
}
464
});
465
466
// Dynamic slice injection
467
const createDynamicStore = () => {
468
let currentReducer = combineSlices();
469
470
const store = configureStore({
471
reducer: currentReducer
472
});
473
474
return {
475
...store,
476
injectSlice: (slice: Slice) => {
477
currentReducer = currentReducer.inject(slice);
478
store.replaceReducer(currentReducer);
479
}
480
};
481
};
482
483
// Usage
484
const dynamicStore = createDynamicStore();
485
486
// Add slices dynamically
487
dynamicStore.injectSlice(counterSlice);
488
dynamicStore.injectSlice(todosSlice);
489
```
490
491
### Development Utilities
492
493
Utilities for development-time debugging and state inspection.
494
495
```typescript { .api }
496
/**
497
* Checks if value is serializable for Redux state
498
* @param value - Value to check
499
* @param path - Current path in object tree
500
* @param isSerializable - Custom serializability checker
501
* @returns First non-serializable value found or false if all serializable
502
*/
503
function findNonSerializableValue(
504
value: any,
505
path?: string,
506
isSerializable?: (value: any) => boolean
507
): false | { keyPath: string; value: any };
508
509
/**
510
* Checks if value is a plain object
511
* @param value - Value to check
512
* @returns True if value is plain object
513
*/
514
function isPlain(value: any): boolean;
515
516
/**
517
* Default immutability check function
518
* @param value - Value to check for immutability
519
* @returns True if value is considered immutable
520
*/
521
function isImmutableDefault(value: any): boolean;
522
```
523
524
**Usage Examples:**
525
526
```typescript
527
import {
528
findNonSerializableValue,
529
isPlain,
530
isImmutableDefault
531
} from '@reduxjs/toolkit';
532
533
// Debug non-serializable values in state
534
const debugState = (state: any) => {
535
const nonSerializable = findNonSerializableValue(state);
536
537
if (nonSerializable) {
538
console.warn(
539
'Non-serializable value found at path:',
540
nonSerializable.keyPath,
541
'Value:',
542
nonSerializable.value
543
);
544
}
545
};
546
547
// Custom serializability checker
548
const customSerializableCheck = (value: any): boolean => {
549
// Allow Date objects
550
if (value instanceof Date) return true;
551
552
// Allow specific function types
553
if (typeof value === 'function' && value.name === 'allowedFunction') {
554
return true;
555
}
556
557
// Default check for other types
558
return isPlain(value) ||
559
typeof value === 'string' ||
560
typeof value === 'number' ||
561
typeof value === 'boolean' ||
562
value === null;
563
};
564
565
// Debug middleware
566
const debugMiddleware: Middleware = (store) => (next) => (action) => {
567
const prevState = store.getState();
568
const result = next(action);
569
const nextState = store.getState();
570
571
// Check for non-serializable values
572
const actionNonSerializable = findNonSerializableValue(action, 'action', customSerializableCheck);
573
const stateNonSerializable = findNonSerializableValue(nextState, 'state', customSerializableCheck);
574
575
if (actionNonSerializable) {
576
console.warn('Non-serializable action:', actionNonSerializable);
577
}
578
579
if (stateNonSerializable) {
580
console.warn('Non-serializable state:', stateNonSerializable);
581
}
582
583
return result;
584
};
585
586
// State validation utility
587
const validateState = (state: any, path = 'state'): string[] => {
588
const errors: string[] = [];
589
590
const checkValue = (value: any, currentPath: string) => {
591
if (!isImmutableDefault(value) && typeof value === 'object' && value !== null) {
592
errors.push(`Mutable object at ${currentPath}`);
593
}
594
595
if (!isPlain(value) && typeof value === 'object' && value !== null) {
596
errors.push(`Non-plain object at ${currentPath}`);
597
}
598
599
if (typeof value === 'object' && value !== null) {
600
Object.keys(value).forEach(key => {
601
checkValue(value[key], `${currentPath}.${key}`);
602
});
603
}
604
};
605
606
checkValue(state, path);
607
return errors;
608
};
609
610
// Usage in development
611
if (process.env.NODE_ENV === 'development') {
612
// Validate store state after each action
613
store.subscribe(() => {
614
const state = store.getState();
615
const errors = validateState(state);
616
617
if (errors.length > 0) {
618
console.group('State validation errors:');
619
errors.forEach(error => console.warn(error));
620
console.groupEnd();
621
}
622
});
623
}
624
```
625
626
### Utility Functions
627
628
Additional utility functions for common Redux patterns.
629
630
```typescript { .api }
631
/**
632
* Generate unique ID string
633
* @param size - Length of generated ID (default: 21)
634
* @returns Random URL-safe string
635
*/
636
function nanoid(size?: number): string;
637
638
/**
639
* Tuple helper for maintaining array types in TypeScript
640
* @param items - Array items to maintain as tuple type
641
* @returns Tuple with preserved types
642
*/
643
function Tuple<T extends readonly unknown[]>(...items: T): T;
644
645
/**
646
* Current value of Immer draft
647
* Re-exported from Immer
648
*/
649
function current<T>(draft: T): T;
650
651
/**
652
* Original value before Immer draft modifications
653
* Re-exported from Immer
654
*/
655
function original<T>(draft: T): T | undefined;
656
657
/**
658
* Create next immutable state (alias for Immer's produce)
659
* Re-exported from Immer
660
*/
661
function createNextState<Base>(base: Base, recipe: (draft: Draft<Base>) => void): Base;
662
663
/**
664
* Freeze object to prevent mutations
665
* Re-exported from Immer
666
*/
667
function freeze<T>(obj: T): T;
668
669
/**
670
* Check if value is Immer draft
671
* Re-exported from Immer
672
*/
673
function isDraft(value: any): boolean;
674
675
/**
676
* Format production error message with error code
677
* Used internally for minified error messages
678
* @param code - Error code number
679
* @returns Formatted error message with link to documentation
680
*/
681
function formatProdErrorMessage(code: number): string;
682
```
683
684
**Usage Examples:**
685
686
```typescript
687
import {
688
nanoid,
689
Tuple,
690
current,
691
original,
692
createNextState,
693
freeze,
694
isDraft,
695
formatProdErrorMessage
696
} from '@reduxjs/toolkit';
697
698
// Generate unique IDs
699
const createTodo = (text: string) => ({
700
id: nanoid(), // Generates unique ID
701
text,
702
completed: false,
703
createdAt: Date.now()
704
});
705
706
// Maintain tuple types
707
const actionTypes = Tuple('INCREMENT', 'DECREMENT', 'RESET');
708
type ActionType = typeof actionTypes[number]; // 'INCREMENT' | 'DECREMENT' | 'RESET'
709
710
// Immer utilities in reducers
711
const complexReducer = createReducer(initialState, (builder) => {
712
builder.addCase(complexUpdate, (state, action) => {
713
// Log current draft state
714
console.log('Current state:', current(state));
715
716
// Log original state before modifications
717
console.log('Original state:', original(state));
718
719
// Check if working with draft
720
if (isDraft(state)) {
721
console.log('Working with Immer draft');
722
}
723
724
// Make complex modifications
725
state.items.forEach(item => {
726
if (item.needsUpdate) {
727
item.lastUpdated = Date.now();
728
}
729
});
730
});
731
});
732
733
// Create immutable state manually
734
const updateStateManually = (currentState: State, updates: Partial<State>) => {
735
return createNextState(currentState, (draft) => {
736
Object.assign(draft, updates);
737
});
738
};
739
740
// Freeze objects for immutability
741
const createImmutableConfig = (config: Config) => {
742
return freeze({
743
...config,
744
metadata: freeze(config.metadata)
745
});
746
};
747
748
// Custom ID generator
749
const createCustomId = () => {
750
const timestamp = Date.now().toString(36);
751
const randomPart = nanoid(8);
752
return `${timestamp}-${randomPart}`;
753
};
754
755
// Format production errors (typically used internally)
756
console.log(formatProdErrorMessage(1));
757
// Output: "Minified Redux Toolkit error #1; visit https://redux-toolkit.js.org/Errors?code=1 for the full message or use the non-minified dev environment for full errors."
758
759
// Type-safe tuple operations
760
const middleware = Tuple(
761
thunkMiddleware,
762
loggerMiddleware,
763
crashReportingMiddleware
764
);
765
766
// Each middleware is properly typed
767
type MiddlewareArray = typeof middleware; // [ThunkMiddleware, LoggerMiddleware, CrashMiddleware]
768
769
// Utility for debugging Immer operations
770
const debugImmerReducer = <S>(
771
initialState: S,
772
name: string
773
) => createReducer(initialState, (builder) => {
774
builder.addDefaultCase((state, action) => {
775
if (isDraft(state)) {
776
console.log(`${name} - Draft state:`, current(state));
777
console.log(`${name} - Original state:`, original(state));
778
console.log(`${name} - Action:`, action);
779
}
780
});
781
});
782
```
783
784
## Advanced Utility Patterns
785
786
### Selector Composition
787
788
```typescript
789
// Compose selectors for complex state selection
790
const createEntitySelectors = <T>(selectEntities: (state: any) => Record<string, T>) => ({
791
selectById: (id: string) => createSelector(
792
[selectEntities],
793
(entities) => entities[id]
794
),
795
796
selectByIds: (ids: string[]) => createSelector(
797
[selectEntities],
798
(entities) => ids.map(id => entities[id]).filter(Boolean)
799
),
800
801
selectAll: createSelector(
802
[selectEntities],
803
(entities) => Object.values(entities)
804
),
805
806
selectCount: createSelector(
807
[selectEntities],
808
(entities) => Object.keys(entities).length
809
)
810
});
811
812
// Usage
813
const userSelectors = createEntitySelectors((state: RootState) => state.users.entities);
814
const postSelectors = createEntitySelectors((state: RootState) => state.posts.entities);
815
```
816
817
### Action Matcher Patterns
818
819
```typescript
820
// Create reusable matcher patterns
821
const createAsyncMatchers = <T extends AsyncThunk<any, any, any>[]>(...thunks: T) => ({
822
pending: isPending(...thunks),
823
fulfilled: isFulfilled(...thunks),
824
rejected: isRejected(...thunks),
825
rejectedWithValue: isRejectedWithValue(...thunks),
826
settled: isAnyOf(isFulfilled(...thunks), isRejected(...thunks)),
827
any: isAsyncThunkAction(...thunks)
828
});
829
830
// Usage
831
const userMatchers = createAsyncMatchers(fetchUser, updateUser, deleteUser);
832
const dataMatchers = createAsyncMatchers(fetchPosts, fetchComments);
833
834
// Apply patterns in reducers
835
builder
836
.addMatcher(userMatchers.pending, handleUserPending)
837
.addMatcher(userMatchers.fulfilled, handleUserFulfilled)
838
.addMatcher(userMatchers.rejected, handleUserRejected);
839
```
840
841
### Development Tool Integration
842
843
```typescript
844
// Enhanced development utilities
845
const createDevUtils = (store: EnhancedStore) => ({
846
logState: () => console.log('Current state:', store.getState()),
847
848
validateState: () => {
849
const state = store.getState();
850
const errors = validateState(state);
851
if (errors.length > 0) {
852
console.warn('State validation errors:', errors);
853
}
854
return errors.length === 0;
855
},
856
857
inspectAction: (action: AnyAction) => {
858
const nonSerializable = findNonSerializableValue(action);
859
return {
860
isSerializable: !nonSerializable,
861
nonSerializablePath: nonSerializable?.keyPath,
862
nonSerializableValue: nonSerializable?.value
863
};
864
},
865
866
timeAction: async (actionCreator: () => AnyAction) => {
867
const startTime = performance.now();
868
const action = actionCreator();
869
await store.dispatch(action);
870
const endTime = performance.now();
871
872
console.log(`Action ${action.type} took ${endTime - startTime}ms`);
873
return endTime - startTime;
874
}
875
});
876
877
// Usage in development
878
if (process.env.NODE_ENV === 'development') {
879
(window as any).devUtils = createDevUtils(store);
880
}
881
```