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

utilities.mddocs/

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

```