0
# Actions & Reducers
1
2
Redux Toolkit provides utilities for creating type-safe action creators and reducers with automatic action type generation and Immer-powered immutable updates.
3
4
## Capabilities
5
6
### Create Action
7
8
Creates a type-safe action creator function with optional payload preparation.
9
10
```typescript { .api }
11
/**
12
* Creates a type-safe action creator with optional payload preparation
13
* @param type - The action type string
14
* @param prepareAction - Optional function to prepare the payload
15
* @returns Action creator function
16
*/
17
function createAction<P = void, T extends string = string>(
18
type: T
19
): PayloadActionCreator<P, T>;
20
21
function createAction<PA extends PrepareAction<any>, T extends string = string>(
22
type: T,
23
prepareAction: PA
24
): PayloadActionCreator<ReturnType<PA>['payload'], T, PA>;
25
26
interface PayloadActionCreator<P, T extends string, PA extends PrepareAction<any> = PrepareAction<any>> {
27
(payload: P): PayloadAction<P, T>;
28
type: T;
29
toString(): T;
30
match(action: AnyAction): action is PayloadAction<P, T>;
31
}
32
33
interface PayloadAction<P = void, T extends string = string, M = never, E = never> {
34
payload: P;
35
type: T;
36
meta?: M;
37
error?: E;
38
}
39
40
interface PrepareAction<P> {
41
(payload: P): { payload: P };
42
(payload: P, meta: any): { payload: P; meta: any };
43
(payload: P, meta: any, error: any): { payload: P; meta: any; error: any };
44
}
45
```
46
47
**Usage Examples:**
48
49
```typescript
50
import { createAction } from '@reduxjs/toolkit';
51
52
// Simple action with payload
53
const increment = createAction<number>('counter/increment');
54
console.log(increment(5));
55
// { type: 'counter/increment', payload: 5 }
56
57
// Action without payload
58
const reset = createAction('counter/reset');
59
console.log(reset());
60
// { type: 'counter/reset' }
61
62
// Action with prepare function
63
const addTodo = createAction('todos/add', (text: string) => ({
64
payload: {
65
text,
66
id: nanoid(),
67
createdAt: Date.now()
68
}
69
}));
70
71
console.log(addTodo('Learn Redux Toolkit'));
72
// {
73
// type: 'todos/add',
74
// payload: {
75
// text: 'Learn Redux Toolkit',
76
// id: 'generated-id',
77
// createdAt: 1640995200000
78
// }
79
// }
80
81
// Action with meta and error handling
82
const fetchUser = createAction('user/fetch', (id: string) => ({
83
payload: id,
84
meta: { requestId: nanoid() }
85
}));
86
```
87
88
### Action Creator Types
89
90
Redux Toolkit provides various action creator types for different use cases.
91
92
```typescript { .api }
93
/**
94
* Action creator that requires a payload
95
*/
96
interface ActionCreatorWithPayload<P, T extends string = string> {
97
(payload: P): PayloadAction<P, T>;
98
type: T;
99
match(action: AnyAction): action is PayloadAction<P, T>;
100
}
101
102
/**
103
* Action creator with optional payload
104
*/
105
interface ActionCreatorWithOptionalPayload<P, T extends string = string> {
106
(payload?: P): PayloadAction<P | undefined, T>;
107
type: T;
108
match(action: AnyAction): action is PayloadAction<P | undefined, T>;
109
}
110
111
/**
112
* Action creator without payload
113
*/
114
interface ActionCreatorWithoutPayload<T extends string = string> {
115
(): PayloadAction<undefined, T>;
116
type: T;
117
match(action: AnyAction): action is PayloadAction<undefined, T>;
118
}
119
120
/**
121
* Action creator with prepared payload
122
*/
123
interface ActionCreatorWithPreparedPayload<Args extends unknown[], P, T extends string = string, E = never, M = never> {
124
(...args: Args): PayloadAction<P, T, M, E>;
125
type: T;
126
match(action: AnyAction): action is PayloadAction<P, T, M, E>;
127
}
128
```
129
130
### Create Reducer
131
132
Creates a reducer function using Immer for immutable updates with a builder callback pattern.
133
134
```typescript { .api }
135
/**
136
* Creates a reducer using Immer for immutable updates
137
* @param initialState - Initial state value or factory function
138
* @param builderCallback - Function that receives builder for adding cases
139
* @param actionMatchers - Additional action matchers (deprecated, use builder)
140
* @param defaultCaseReducer - Default case reducer (deprecated, use builder)
141
* @returns Reducer with getInitialState method
142
*/
143
function createReducer<S>(
144
initialState: S | (() => S),
145
builderCallback: (builder: ActionReducerMapBuilder<S>) => void
146
): ReducerWithInitialState<S>;
147
148
interface ReducerWithInitialState<S> extends Reducer<S> {
149
getInitialState(): S;
150
}
151
152
interface ActionReducerMapBuilder<State> {
153
/** Add a case reducer for a specific action type */
154
addCase<ActionCreator extends TypedActionCreator<string>>(
155
actionCreator: ActionCreator,
156
reducer: CaseReducer<State, ReturnType<ActionCreator>>
157
): ActionReducerMapBuilder<State>;
158
159
/** Add a case reducer for a specific action type string */
160
addCase<Type extends string, A extends Action<Type>>(
161
type: Type,
162
reducer: CaseReducer<State, A>
163
): ActionReducerMapBuilder<State>;
164
165
/** Add a matcher for actions */
166
addMatcher<A extends AnyAction>(
167
matcher: ActionMatcher<A>,
168
reducer: CaseReducer<State, A>
169
): ActionReducerMapBuilder<State>;
170
171
/** Add a default case reducer */
172
addDefaultCase(reducer: CaseReducer<State, AnyAction>): ActionReducerMapBuilder<State>;
173
}
174
175
type CaseReducer<S = any, A extends Action = AnyAction> = (state: Draft<S>, action: A) => S | void | Draft<S>;
176
```
177
178
**Usage Examples:**
179
180
```typescript
181
import { createReducer, createAction } from '@reduxjs/toolkit';
182
183
interface CounterState {
184
value: number;
185
}
186
187
const increment = createAction<number>('increment');
188
const decrement = createAction<number>('decrement');
189
const reset = createAction('reset');
190
191
const counterReducer = createReducer(
192
{ value: 0 } as CounterState,
193
(builder) => {
194
builder
195
.addCase(increment, (state, action) => {
196
// Immer allows "mutating" the draft state
197
state.value += action.payload;
198
})
199
.addCase(decrement, (state, action) => {
200
state.value -= action.payload;
201
})
202
.addCase(reset, (state) => {
203
state.value = 0;
204
})
205
.addMatcher(
206
(action): action is PayloadAction<number> =>
207
typeof action.payload === 'number',
208
(state, action) => {
209
// Handle all actions with number payloads
210
}
211
)
212
.addDefaultCase((state, action) => {
213
// Handle any other actions
214
console.log('Unhandled action:', action.type);
215
});
216
}
217
);
218
219
// Alternative: return new state instead of mutating
220
const todoReducer = createReducer([], (builder) => {
221
builder.addCase(addTodo, (state, action) => {
222
// Can return new state instead of mutating
223
return [...state, action.payload];
224
});
225
});
226
```
227
228
### Create Slice
229
230
Automatically generates action creators and a reducer from a single configuration object.
231
232
```typescript { .api }
233
/**
234
* Automatically generates action creators and reducer from configuration
235
* @param options - Slice configuration options
236
* @returns Slice object with actions, reducer, and utilities
237
*/
238
function createSlice<
239
State,
240
CaseReducers extends SliceCaseReducers<State>,
241
Name extends string = string,
242
ReducerPath extends string = Name,
243
Selectors extends SliceSelectors<State> = {}
244
>(options: CreateSliceOptions<State, CaseReducers, Name, ReducerPath, Selectors>): Slice<State, CaseReducers, Name, ReducerPath, Selectors>;
245
246
interface CreateSliceOptions<State, CR extends SliceCaseReducers<State>, Name extends string, ReducerPath extends string, Selectors extends SliceSelectors<State>> {
247
/** Slice name used to generate action types */
248
name: Name;
249
/** Path in the store where this slice's state will be located */
250
reducerPath?: ReducerPath;
251
/** Initial state value or factory function */
252
initialState: State | (() => State);
253
/** Case reducer functions or creator functions */
254
reducers: ValidateSliceCaseReducers<State, CR>;
255
/** Builder callback for additional action types */
256
extraReducers?: (builder: ActionReducerMapBuilder<NoInfer<State>>) => void;
257
/** Selector functions */
258
selectors?: Selectors;
259
}
260
261
interface Slice<State = any, CaseReducers = {}, Name extends string = string, ReducerPath extends string = Name, Selectors = {}> {
262
/** The slice name */
263
name: Name;
264
/** The reducer path in the store */
265
reducerPath: ReducerPath;
266
/** The combined reducer function */
267
reducer: Reducer<State>;
268
/** Generated action creators */
269
actions: CaseReducerActions<CaseReducers, Name>;
270
/** Individual case reducer functions */
271
caseReducers: CaseReducers;
272
/** Returns the initial state */
273
getInitialState(): State;
274
/** Creates selectors bound to the slice state */
275
getSelectors(): Selectors & SliceSelectors<State>;
276
/** Creates selectors with custom state selector */
277
getSelectors<RootState>(selectState: (state: RootState) => State): Selectors & SliceSelectors<State>;
278
/** Selectors with default slice state selector */
279
selectors: Selectors & SliceSelectors<State>;
280
/** Selector to get the slice state */
281
selectSlice(state: { [K in ReducerPath]: State }): State;
282
/** Inject into a combined reducer */
283
injectInto(injectable: WithSlice<Slice<State, CaseReducers, Name, ReducerPath, Selectors>>, config?: { reducerPath?: any }): any;
284
}
285
```
286
287
**Usage Examples:**
288
289
```typescript
290
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
291
292
interface CounterState {
293
value: number;
294
status: 'idle' | 'loading' | 'succeeded' | 'failed';
295
}
296
297
const counterSlice = createSlice({
298
name: 'counter',
299
initialState: { value: 0, status: 'idle' } as CounterState,
300
reducers: {
301
// Standard reducer with payload
302
incrementByAmount: (state, action: PayloadAction<number>) => {
303
state.value += action.payload;
304
},
305
306
// Reducer without payload
307
increment: (state) => {
308
state.value += 1;
309
},
310
311
// Reducer with prepare callback
312
incrementByAmountWithId: {
313
reducer: (state, action: PayloadAction<{ amount: number; id: string }>) => {
314
state.value += action.payload.amount;
315
},
316
prepare: (amount: number) => ({
317
payload: { amount, id: nanoid() }
318
})
319
},
320
321
// Reset to initial state
322
reset: () => ({ value: 0, status: 'idle' } as CounterState)
323
},
324
325
// Handle external actions
326
extraReducers: (builder) => {
327
builder
328
.addCase(fetchUserById.pending, (state) => {
329
state.status = 'loading';
330
})
331
.addCase(fetchUserById.fulfilled, (state) => {
332
state.status = 'succeeded';
333
})
334
.addCase(fetchUserById.rejected, (state) => {
335
state.status = 'failed';
336
});
337
},
338
339
// Selectors
340
selectors: {
341
selectValue: (state) => state.value,
342
selectStatus: (state) => state.status,
343
selectIsLoading: (state) => state.status === 'loading'
344
}
345
});
346
347
// Export actions (automatically generated)
348
export const { increment, incrementByAmount, incrementByAmountWithId, reset } = counterSlice.actions;
349
350
// Export selectors
351
export const { selectValue, selectStatus, selectIsLoading } = counterSlice.selectors;
352
353
// Export reducer
354
export default counterSlice.reducer;
355
356
// Usage with typed selectors
357
const selectCounterValue = counterSlice.selectors.selectValue;
358
const value = selectCounterValue(rootState); // Automatically typed
359
360
// Create selectors with custom state selector
361
const counterSelectors = counterSlice.getSelectors((state: RootState) => state.counter);
362
const currentValue = counterSelectors.selectValue(rootState);
363
```
364
365
### Case Reducer Types
366
367
```typescript { .api }
368
/**
369
* Map of case reducer functions for a slice
370
*/
371
type SliceCaseReducers<State> = {
372
[K: string]:
373
| CaseReducer<State, PayloadAction<any>>
374
| CaseReducerWithPrepare<State, PayloadAction<any, string, any, any>>;
375
};
376
377
/**
378
* Case reducer with prepare method
379
*/
380
interface CaseReducerWithPrepare<State, Action extends PayloadAction> {
381
reducer: CaseReducer<State, Action>;
382
prepare: PrepareAction<Action['payload']>;
383
}
384
385
/**
386
* Generated action creators from slice case reducers
387
*/
388
type CaseReducerActions<CaseReducers, SliceName extends string> = {
389
[Type in keyof CaseReducers]: CaseReducers[Type] extends { prepare: any }
390
? ActionCreatorForCaseReducerWithPrepare<CaseReducers[Type], SliceName>
391
: ActionCreatorForCaseReducer<CaseReducers[Type], SliceName>;
392
};
393
394
/**
395
* Validation helper for slice case reducers
396
*/
397
type ValidateSliceCaseReducers<S, ACR extends SliceCaseReducers<S>> = ACR & {
398
[T in keyof ACR]: ACR[T] extends {
399
reducer(s: S, action?: infer A): any;
400
}
401
? {
402
prepare(...a: never[]): Omit<A, 'type'>;
403
}
404
: {};
405
};
406
```
407
408
### Action Utilities
409
410
Redux Toolkit provides utilities for working with actions and action creators.
411
412
```typescript { .api }
413
/**
414
* Type guard to check if a value is an RTK action creator
415
* @param action - Value to check
416
* @returns True if the value is an action creator
417
*/
418
function isActionCreator(action: any): action is (...args: any[]) => AnyAction;
419
420
/**
421
* Checks if an action follows the Flux Standard Action format
422
* @param action - Action to check
423
* @returns True if action is FSA compliant
424
*/
425
function isFSA(action: unknown): action is {
426
type: string;
427
payload?: any;
428
error?: boolean;
429
meta?: any;
430
};
431
432
/** Alias for isFSA */
433
function isFluxStandardAction(action: unknown): action is {
434
type: string;
435
payload?: any;
436
error?: boolean;
437
meta?: any;
438
};
439
```
440
441
**Usage Examples:**
442
443
```typescript
444
import { isActionCreator, isFSA, createAction } from '@reduxjs/toolkit';
445
446
const increment = createAction('increment');
447
const plainAction = { type: 'PLAIN_ACTION' };
448
449
console.log(isActionCreator(increment)); // true
450
console.log(isActionCreator(plainAction)); // false
451
452
console.log(isFSA(increment(5))); // true
453
console.log(isFSA({ type: 'TEST', invalid: true })); // false
454
```
455
456
## Advanced Patterns
457
458
### Builder Callback Pattern
459
460
```typescript
461
// Using builder callback for complex reducer logic
462
const complexSlice = createSlice({
463
name: 'complex',
464
initialState: { items: [], status: 'idle' },
465
reducers: {
466
itemAdded: (state, action) => {
467
state.items.push(action.payload);
468
}
469
},
470
extraReducers: (builder) => {
471
// Handle async thunk actions
472
builder
473
.addCase(fetchItems.pending, (state) => {
474
state.status = 'loading';
475
})
476
.addCase(fetchItems.fulfilled, (state, action) => {
477
state.status = 'succeeded';
478
state.items = action.payload;
479
})
480
// Handle multiple action types with matcher
481
.addMatcher(
482
(action) => action.type.endsWith('/pending'),
483
(state) => {
484
state.status = 'loading';
485
}
486
)
487
// Default handler
488
.addDefaultCase((state, action) => {
489
console.log('Unhandled action:', action);
490
});
491
}
492
});
493
```
494
495
### Build Create Slice
496
497
Advanced slice creation with custom reducer creators like async thunks.
498
499
```typescript { .api }
500
/**
501
* Creates a custom createSlice function with additional reducer creators
502
* @param config - Configuration with custom creators
503
* @returns Custom createSlice function
504
*/
505
function buildCreateSlice(config?: {
506
creators?: {
507
asyncThunk?: typeof asyncThunkCreator;
508
};
509
}): typeof createSlice;
510
511
/**
512
* Async thunk creator symbol for buildCreateSlice
513
* Used to define async reducers directly in slice definitions
514
*/
515
const asyncThunkCreator: {
516
[Symbol.for('rtk-slice-createasyncthunk')]: typeof createAsyncThunk;
517
};
518
519
/**
520
* Enum defining different types of reducers
521
*/
522
enum ReducerType {
523
reducer = 'reducer',
524
reducerWithPrepare = 'reducerWithPrepare',
525
asyncThunk = 'asyncThunk',
526
}
527
```
528
529
**Usage Examples:**
530
531
```typescript
532
import { buildCreateSlice, asyncThunkCreator } from '@reduxjs/toolkit';
533
534
// Create custom slice builder with async thunk support
535
const createSliceWithAsyncThunks = buildCreateSlice({
536
creators: { asyncThunk: asyncThunkCreator }
537
});
538
539
// Use it to create slices with async reducers
540
const userSlice = createSliceWithAsyncThunks({
541
name: 'user',
542
initialState: { data: null, loading: false },
543
reducers: (create) => ({
544
// Regular reducer
545
clearUser: create.reducer((state) => {
546
state.data = null;
547
}),
548
// Async thunk reducer defined inline
549
fetchUser: create.asyncThunk(
550
async (userId: string) => {
551
const response = await fetch(`/api/users/${userId}`);
552
return response.json();
553
},
554
{
555
pending: (state) => {
556
state.loading = true;
557
},
558
fulfilled: (state, action) => {
559
state.loading = false;
560
state.data = action.payload;
561
},
562
rejected: (state) => {
563
state.loading = false;
564
}
565
}
566
)
567
})
568
});
569
```
570
571
### Slice Injection
572
573
```typescript
574
// Dynamically inject slices into combined reducers
575
import { combineSlices } from '@reduxjs/toolkit';
576
577
const rootReducer = combineSlices(counterSlice, todosSlice);
578
579
// Later, inject a new slice
580
const withAuthSlice = authSlice.injectInto(rootReducer);
581
```
582
583
### Custom Prepare Functions
584
585
```typescript
586
const todosSlice = createSlice({
587
name: 'todos',
588
initialState: [],
589
reducers: {
590
todoAdded: {
591
reducer: (state, action) => {
592
state.push(action.payload);
593
},
594
prepare: (text: string) => {
595
return {
596
payload: {
597
id: nanoid(),
598
text,
599
completed: false,
600
createdAt: Date.now()
601
},
602
meta: {
603
timestamp: Date.now()
604
}
605
};
606
}
607
}
608
}
609
});
610
```