1 | import type { Action, UnknownAction, Reducer } from 'redux'
|
2 | import type { Selector } from 'reselect'
|
3 | import type {
|
4 | ActionCreatorWithoutPayload,
|
5 | PayloadAction,
|
6 | PayloadActionCreator,
|
7 | PrepareAction,
|
8 | _ActionCreatorWithPreparedPayload,
|
9 | } from './createAction'
|
10 | import { createAction } from './createAction'
|
11 | import type {
|
12 | ActionMatcherDescriptionCollection,
|
13 | CaseReducer,
|
14 | ReducerWithInitialState,
|
15 | } from './createReducer'
|
16 | import { createReducer } from './createReducer'
|
17 | import type { ActionReducerMapBuilder, TypedActionCreator } from './mapBuilders'
|
18 | import { executeReducerBuilderCallback } from './mapBuilders'
|
19 | import type { Id, Tail, TypeGuard } from './tsHelpers'
|
20 | import type { InjectConfig } from './combineSlices'
|
21 | import type {
|
22 | AsyncThunk,
|
23 | AsyncThunkConfig,
|
24 | AsyncThunkOptions,
|
25 | AsyncThunkPayloadCreator,
|
26 | OverrideThunkApiConfigs,
|
27 | } from './createAsyncThunk'
|
28 | import { createAsyncThunk as _createAsyncThunk } from './createAsyncThunk'
|
29 | import { emplace } from './utils'
|
30 |
|
31 | const asyncThunkSymbol = Symbol.for(
|
32 | 'rtk-slice-createasyncthunk',
|
33 | )
|
34 |
|
35 | export const asyncThunkCreator: {
|
36 | [asyncThunkSymbol]: typeof _createAsyncThunk
|
37 | } = {
|
38 | [asyncThunkSymbol]: _createAsyncThunk,
|
39 | }
|
40 |
|
41 | interface InjectIntoConfig<NewReducerPath extends string> extends InjectConfig {
|
42 | reducerPath?: NewReducerPath
|
43 | }
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 | export interface Slice<
|
51 | State = any,
|
52 | CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>,
|
53 | Name extends string = string,
|
54 | ReducerPath extends string = Name,
|
55 | Selectors extends SliceSelectors<State> = SliceSelectors<State>,
|
56 | > {
|
57 | |
58 |
|
59 |
|
60 | name: Name
|
61 |
|
62 | |
63 |
|
64 |
|
65 | reducerPath: ReducerPath
|
66 |
|
67 | |
68 |
|
69 |
|
70 | reducer: Reducer<State>
|
71 |
|
72 | |
73 |
|
74 |
|
75 |
|
76 | actions: CaseReducerActions<CaseReducers, Name>
|
77 |
|
78 | |
79 |
|
80 |
|
81 |
|
82 | caseReducers: SliceDefinedCaseReducers<CaseReducers>
|
83 |
|
84 | |
85 |
|
86 |
|
87 |
|
88 | getInitialState: () => State
|
89 |
|
90 | |
91 |
|
92 |
|
93 | getSelectors(): Id<SliceDefinedSelectors<State, Selectors, State>>
|
94 |
|
95 | |
96 |
|
97 |
|
98 | getSelectors<RootState>(
|
99 | selectState: (rootState: RootState) => State,
|
100 | ): Id<SliceDefinedSelectors<State, Selectors, RootState>>
|
101 |
|
102 | |
103 |
|
104 |
|
105 |
|
106 |
|
107 | get selectors(): Id<
|
108 | SliceDefinedSelectors<State, Selectors, { [K in ReducerPath]: State }>
|
109 | >
|
110 |
|
111 | |
112 |
|
113 |
|
114 | injectInto<NewReducerPath extends string = ReducerPath>(
|
115 | this: this,
|
116 | injectable: {
|
117 | inject: (
|
118 | slice: { reducerPath: string; reducer: Reducer },
|
119 | config?: InjectConfig,
|
120 | ) => void
|
121 | },
|
122 | config?: InjectIntoConfig<NewReducerPath>,
|
123 | ): InjectedSlice<State, CaseReducers, Name, NewReducerPath, Selectors>
|
124 |
|
125 | |
126 |
|
127 |
|
128 |
|
129 |
|
130 | selectSlice(state: { [K in ReducerPath]: State }): State
|
131 | }
|
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 | interface InjectedSlice<
|
139 | State = any,
|
140 | CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>,
|
141 | Name extends string = string,
|
142 | ReducerPath extends string = Name,
|
143 | Selectors extends SliceSelectors<State> = SliceSelectors<State>,
|
144 | > extends Omit<
|
145 | Slice<State, CaseReducers, Name, ReducerPath, Selectors>,
|
146 | 'getSelectors' | 'selectors'
|
147 | > {
|
148 | |
149 |
|
150 |
|
151 | getSelectors(): Id<SliceDefinedSelectors<State, Selectors, State | undefined>>
|
152 |
|
153 | |
154 |
|
155 |
|
156 | getSelectors<RootState>(
|
157 | selectState: (rootState: RootState) => State | undefined,
|
158 | ): Id<SliceDefinedSelectors<State, Selectors, RootState>>
|
159 |
|
160 | |
161 |
|
162 |
|
163 |
|
164 |
|
165 | get selectors(): Id<
|
166 | SliceDefinedSelectors<
|
167 | State,
|
168 | Selectors,
|
169 | { [K in ReducerPath]?: State | undefined }
|
170 | >
|
171 | >
|
172 |
|
173 | |
174 |
|
175 |
|
176 |
|
177 |
|
178 | selectSlice(state: { [K in ReducerPath]?: State | undefined }): State
|
179 | }
|
180 |
|
181 |
|
182 |
|
183 |
|
184 |
|
185 |
|
186 | export interface CreateSliceOptions<
|
187 | State = any,
|
188 | CR extends SliceCaseReducers<State> = SliceCaseReducers<State>,
|
189 | Name extends string = string,
|
190 | ReducerPath extends string = Name,
|
191 | Selectors extends SliceSelectors<State> = SliceSelectors<State>,
|
192 | > {
|
193 | |
194 |
|
195 |
|
196 | name: Name
|
197 |
|
198 | |
199 |
|
200 |
|
201 | reducerPath?: ReducerPath
|
202 |
|
203 | |
204 |
|
205 |
|
206 | initialState: State | (() => State)
|
207 |
|
208 | /**
|
209 | * A mapping from action types to action-type-specific *case reducer*
|
210 | * functions. For every action type, a matching action creator will be
|
211 | * generated using `createAction()`.
|
212 | */
|
213 | reducers:
|
214 | | ValidateSliceCaseReducers<State, CR>
|
215 | | ((creators: ReducerCreators<State>) => CR)
|
216 |
|
217 | /**
|
218 | * A callback that receives a *builder* object to define
|
219 | * case reducers via calls to `builder.addCase(actionCreatorOrType, reducer)`.
|
220 | *
|
221 | *
|
222 | * @example
|
223 | ```ts
|
224 | import { createAction, createSlice, Action } from '@reduxjs/toolkit'
|
225 | const incrementBy = createAction<number>('incrementBy')
|
226 | const decrement = createAction('decrement')
|
227 |
|
228 | interface RejectedAction extends Action {
|
229 | error: Error
|
230 | }
|
231 |
|
232 | function isRejectedAction(action: Action): action is RejectedAction {
|
233 | return action.type.endsWith('rejected')
|
234 | }
|
235 |
|
236 | createSlice({
|
237 | name: 'counter',
|
238 | initialState: 0,
|
239 | reducers: {},
|
240 | extraReducers: builder => {
|
241 | builder
|
242 | .addCase(incrementBy, (state, action) => {
|
243 |
|
244 | })
|
245 |
|
246 | .addCase(decrement, (state, action) => {})
|
247 |
|
248 | .addMatcher(
|
249 | isRejectedAction,
|
250 |
|
251 | (state, action) => {}
|
252 | )
|
253 |
|
254 | .addDefaultCase((state, action) => {})
|
255 | }
|
256 | })
|
257 | ```
|
258 | */
|
259 | extraReducers?: (builder: ActionReducerMapBuilder<State>) => void
|
260 |
|
261 | |
262 |
|
263 |
|
264 | selectors?: Selectors
|
265 | }
|
266 |
|
267 | export enum ReducerType {
|
268 | reducer = 'reducer',
|
269 | reducerWithPrepare = 'reducerWithPrepare',
|
270 | asyncThunk = 'asyncThunk',
|
271 | }
|
272 |
|
273 | interface ReducerDefinition<T extends ReducerType = ReducerType> {
|
274 | _reducerDefinitionType: T
|
275 | }
|
276 |
|
277 | export interface CaseReducerDefinition<
|
278 | S = any,
|
279 | A extends Action = UnknownAction,
|
280 | > extends CaseReducer<S, A>,
|
281 | ReducerDefinition<ReducerType.reducer> {}
|
282 |
|
283 |
|
284 |
|
285 |
|
286 |
|
287 |
|
288 | export type CaseReducerWithPrepare<State, Action extends PayloadAction> = {
|
289 | reducer: CaseReducer<State, Action>
|
290 | prepare: PrepareAction<Action['payload']>
|
291 | }
|
292 |
|
293 | export interface CaseReducerWithPrepareDefinition<
|
294 | State,
|
295 | Action extends PayloadAction,
|
296 | > extends CaseReducerWithPrepare<State, Action>,
|
297 | ReducerDefinition<ReducerType.reducerWithPrepare> {}
|
298 |
|
299 | export interface AsyncThunkSliceReducerConfig<
|
300 | State,
|
301 | ThunkArg extends any,
|
302 | Returned = unknown,
|
303 | ThunkApiConfig extends AsyncThunkConfig = {},
|
304 | > {
|
305 | pending?: CaseReducer<
|
306 | State,
|
307 | ReturnType<AsyncThunk<Returned, ThunkArg, ThunkApiConfig>['pending']>
|
308 | >
|
309 | rejected?: CaseReducer<
|
310 | State,
|
311 | ReturnType<AsyncThunk<Returned, ThunkArg, ThunkApiConfig>['rejected']>
|
312 | >
|
313 | fulfilled?: CaseReducer<
|
314 | State,
|
315 | ReturnType<AsyncThunk<Returned, ThunkArg, ThunkApiConfig>['fulfilled']>
|
316 | >
|
317 | settled?: CaseReducer<
|
318 | State,
|
319 | ReturnType<
|
320 | AsyncThunk<Returned, ThunkArg, ThunkApiConfig>['rejected' | 'fulfilled']
|
321 | >
|
322 | >
|
323 | options?: AsyncThunkOptions<ThunkArg, ThunkApiConfig>
|
324 | }
|
325 |
|
326 | export interface AsyncThunkSliceReducerDefinition<
|
327 | State,
|
328 | ThunkArg extends any,
|
329 | Returned = unknown,
|
330 | ThunkApiConfig extends AsyncThunkConfig = {},
|
331 | > extends AsyncThunkSliceReducerConfig<
|
332 | State,
|
333 | ThunkArg,
|
334 | Returned,
|
335 | ThunkApiConfig
|
336 | >,
|
337 | ReducerDefinition<ReducerType.asyncThunk> {
|
338 | payloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg, ThunkApiConfig>
|
339 | }
|
340 |
|
341 |
|
342 |
|
343 |
|
344 | type PreventCircular<ThunkApiConfig> = {
|
345 | [K in keyof ThunkApiConfig]: K extends 'state' | 'dispatch'
|
346 | ? never
|
347 | : ThunkApiConfig[K]
|
348 | }
|
349 |
|
350 | interface AsyncThunkCreator<
|
351 | State,
|
352 | CurriedThunkApiConfig extends
|
353 | PreventCircular<AsyncThunkConfig> = PreventCircular<AsyncThunkConfig>,
|
354 | > {
|
355 | <Returned, ThunkArg = void>(
|
356 | payloadCreator: AsyncThunkPayloadCreator<
|
357 | Returned,
|
358 | ThunkArg,
|
359 | CurriedThunkApiConfig
|
360 | >,
|
361 | config?: AsyncThunkSliceReducerConfig<
|
362 | State,
|
363 | ThunkArg,
|
364 | Returned,
|
365 | CurriedThunkApiConfig
|
366 | >,
|
367 | ): AsyncThunkSliceReducerDefinition<
|
368 | State,
|
369 | ThunkArg,
|
370 | Returned,
|
371 | CurriedThunkApiConfig
|
372 | >
|
373 | <
|
374 | Returned,
|
375 | ThunkArg,
|
376 | ThunkApiConfig extends PreventCircular<AsyncThunkConfig> = {},
|
377 | >(
|
378 | payloadCreator: AsyncThunkPayloadCreator<
|
379 | Returned,
|
380 | ThunkArg,
|
381 | ThunkApiConfig
|
382 | >,
|
383 | config?: AsyncThunkSliceReducerConfig<
|
384 | State,
|
385 | ThunkArg,
|
386 | Returned,
|
387 | ThunkApiConfig
|
388 | >,
|
389 | ): AsyncThunkSliceReducerDefinition<State, ThunkArg, Returned, ThunkApiConfig>
|
390 | withTypes<
|
391 | ThunkApiConfig extends PreventCircular<AsyncThunkConfig>,
|
392 | >(): AsyncThunkCreator<
|
393 | State,
|
394 | OverrideThunkApiConfigs<CurriedThunkApiConfig, ThunkApiConfig>
|
395 | >
|
396 | }
|
397 |
|
398 | export interface ReducerCreators<State> {
|
399 | reducer(
|
400 | caseReducer: CaseReducer<State, PayloadAction>,
|
401 | ): CaseReducerDefinition<State, PayloadAction>
|
402 | reducer<Payload>(
|
403 | caseReducer: CaseReducer<State, PayloadAction<Payload>>,
|
404 | ): CaseReducerDefinition<State, PayloadAction<Payload>>
|
405 |
|
406 | asyncThunk: AsyncThunkCreator<State>
|
407 |
|
408 | preparedReducer<Prepare extends PrepareAction<any>>(
|
409 | prepare: Prepare,
|
410 | reducer: CaseReducer<
|
411 | State,
|
412 | ReturnType<_ActionCreatorWithPreparedPayload<Prepare>>
|
413 | >,
|
414 | ): {
|
415 | _reducerDefinitionType: ReducerType.reducerWithPrepare
|
416 | prepare: Prepare
|
417 | reducer: CaseReducer<
|
418 | State,
|
419 | ReturnType<_ActionCreatorWithPreparedPayload<Prepare>>
|
420 | >
|
421 | }
|
422 | }
|
423 |
|
424 |
|
425 |
|
426 |
|
427 |
|
428 |
|
429 | export type SliceCaseReducers<State> =
|
430 | | Record<string, ReducerDefinition>
|
431 | | Record<
|
432 | string,
|
433 | | CaseReducer<State, PayloadAction<any>>
|
434 | | CaseReducerWithPrepare<State, PayloadAction<any, string, any, any>>
|
435 | >
|
436 |
|
437 |
|
438 |
|
439 |
|
440 | export type SliceSelectors<State> = {
|
441 | [K: string]: (sliceState: State, ...args: any[]) => any
|
442 | }
|
443 |
|
444 | type SliceActionType<
|
445 | SliceName extends string,
|
446 | ActionName extends keyof any,
|
447 | > = ActionName extends string | number ? `${SliceName}/${ActionName}` : string
|
448 |
|
449 |
|
450 |
|
451 |
|
452 |
|
453 |
|
454 | export type CaseReducerActions<
|
455 | CaseReducers extends SliceCaseReducers<any>,
|
456 | SliceName extends string,
|
457 | > = {
|
458 | [Type in keyof CaseReducers]: CaseReducers[Type] extends infer Definition
|
459 | ? Definition extends { prepare: any }
|
460 | ? ActionCreatorForCaseReducerWithPrepare<
|
461 | Definition,
|
462 | SliceActionType<SliceName, Type>
|
463 | >
|
464 | : Definition extends AsyncThunkSliceReducerDefinition<
|
465 | any,
|
466 | infer ThunkArg,
|
467 | infer Returned,
|
468 | infer ThunkApiConfig
|
469 | >
|
470 | ? AsyncThunk<Returned, ThunkArg, ThunkApiConfig>
|
471 | : Definition extends { reducer: any }
|
472 | ? ActionCreatorForCaseReducer<
|
473 | Definition['reducer'],
|
474 | SliceActionType<SliceName, Type>
|
475 | >
|
476 | : ActionCreatorForCaseReducer<
|
477 | Definition,
|
478 | SliceActionType<SliceName, Type>
|
479 | >
|
480 | : never
|
481 | }
|
482 |
|
483 |
|
484 |
|
485 |
|
486 |
|
487 |
|
488 | type ActionCreatorForCaseReducerWithPrepare<
|
489 | CR extends { prepare: any },
|
490 | Type extends string,
|
491 | > = _ActionCreatorWithPreparedPayload<CR['prepare'], Type>
|
492 |
|
493 |
|
494 |
|
495 |
|
496 |
|
497 |
|
498 | type ActionCreatorForCaseReducer<CR, Type extends string> = CR extends (
|
499 | state: any,
|
500 | action: infer Action,
|
501 | ) => any
|
502 | ? Action extends { payload: infer P }
|
503 | ? PayloadActionCreator<P, Type>
|
504 | : ActionCreatorWithoutPayload<Type>
|
505 | : ActionCreatorWithoutPayload<Type>
|
506 |
|
507 |
|
508 |
|
509 |
|
510 |
|
511 |
|
512 |
|
513 | type SliceDefinedCaseReducers<CaseReducers extends SliceCaseReducers<any>> = {
|
514 | [Type in keyof CaseReducers]: CaseReducers[Type] extends infer Definition
|
515 | ? Definition extends AsyncThunkSliceReducerDefinition<any, any, any, any>
|
516 | ? Id<
|
517 | Pick<
|
518 | Required<Definition>,
|
519 | 'fulfilled' | 'rejected' | 'pending' | 'settled'
|
520 | >
|
521 | >
|
522 | : Definition extends {
|
523 | reducer: infer Reducer
|
524 | }
|
525 | ? Reducer
|
526 | : Definition
|
527 | : never
|
528 | }
|
529 |
|
530 | type RemappedSelector<S extends Selector, NewState> =
|
531 | S extends Selector<any, infer R, infer P>
|
532 | ? Selector<NewState, R, P> & { unwrapped: S }
|
533 | : never
|
534 |
|
535 |
|
536 |
|
537 |
|
538 |
|
539 |
|
540 | type SliceDefinedSelectors<
|
541 | State,
|
542 | Selectors extends SliceSelectors<State>,
|
543 | RootState,
|
544 | > = {
|
545 | [K in keyof Selectors as string extends K ? never : K]: RemappedSelector<
|
546 | Selectors[K],
|
547 | RootState
|
548 | >
|
549 | }
|
550 |
|
551 |
|
552 |
|
553 |
|
554 |
|
555 |
|
556 |
|
557 |
|
558 |
|
559 |
|
560 |
|
561 |
|
562 |
|
563 | export type ValidateSliceCaseReducers<
|
564 | S,
|
565 | ACR extends SliceCaseReducers<S>,
|
566 | > = ACR & {
|
567 | [T in keyof ACR]: ACR[T] extends {
|
568 | reducer(s: S, action?: infer A): any
|
569 | }
|
570 | ? {
|
571 | prepare(...a: never[]): Omit<A, 'type'>
|
572 | }
|
573 | : {}
|
574 | }
|
575 |
|
576 | function getType(slice: string, actionKey: string): string {
|
577 | return `${slice}/${actionKey}`
|
578 | }
|
579 |
|
580 | interface BuildCreateSliceConfig {
|
581 | creators?: {
|
582 | asyncThunk?: typeof asyncThunkCreator
|
583 | }
|
584 | }
|
585 |
|
586 | export function buildCreateSlice({ creators }: BuildCreateSliceConfig = {}) {
|
587 | const cAT = creators?.asyncThunk?.[asyncThunkSymbol]
|
588 | return function createSlice<
|
589 | State,
|
590 | CaseReducers extends SliceCaseReducers<State>,
|
591 | Name extends string,
|
592 | Selectors extends SliceSelectors<State>,
|
593 | ReducerPath extends string = Name,
|
594 | >(
|
595 | options: CreateSliceOptions<
|
596 | State,
|
597 | CaseReducers,
|
598 | Name,
|
599 | ReducerPath,
|
600 | Selectors
|
601 | >,
|
602 | ): Slice<State, CaseReducers, Name, ReducerPath, Selectors> {
|
603 | const { name, reducerPath = name as unknown as ReducerPath } = options
|
604 | if (!name) {
|
605 | throw new Error('`name` is a required option for createSlice')
|
606 | }
|
607 |
|
608 | if (
|
609 | typeof process !== 'undefined' &&
|
610 | process.env.NODE_ENV === 'development'
|
611 | ) {
|
612 | if (options.initialState === undefined) {
|
613 | console.error(
|
614 | 'You must provide an `initialState` value that is not `undefined`. You may have misspelled `initialState`',
|
615 | )
|
616 | }
|
617 | }
|
618 |
|
619 | const reducers =
|
620 | (typeof options.reducers === 'function'
|
621 | ? options.reducers(buildReducerCreators<State>())
|
622 | : options.reducers) || {}
|
623 |
|
624 | const reducerNames = Object.keys(reducers)
|
625 |
|
626 | const context: ReducerHandlingContext<State> = {
|
627 | sliceCaseReducersByName: {},
|
628 | sliceCaseReducersByType: {},
|
629 | actionCreators: {},
|
630 | sliceMatchers: [],
|
631 | }
|
632 |
|
633 | const contextMethods: ReducerHandlingContextMethods<State> = {
|
634 | addCase(
|
635 | typeOrActionCreator: string | TypedActionCreator<any>,
|
636 | reducer: CaseReducer<State>,
|
637 | ) {
|
638 | const type =
|
639 | typeof typeOrActionCreator === 'string'
|
640 | ? typeOrActionCreator
|
641 | : typeOrActionCreator.type
|
642 | if (!type) {
|
643 | throw new Error(
|
644 | '`context.addCase` cannot be called with an empty action type',
|
645 | )
|
646 | }
|
647 | if (type in context.sliceCaseReducersByType) {
|
648 | throw new Error(
|
649 | '`context.addCase` cannot be called with two reducers for the same action type: ' +
|
650 | type,
|
651 | )
|
652 | }
|
653 | context.sliceCaseReducersByType[type] = reducer
|
654 | return contextMethods
|
655 | },
|
656 | addMatcher(matcher, reducer) {
|
657 | context.sliceMatchers.push({ matcher, reducer })
|
658 | return contextMethods
|
659 | },
|
660 | exposeAction(name, actionCreator) {
|
661 | context.actionCreators[name] = actionCreator
|
662 | return contextMethods
|
663 | },
|
664 | exposeCaseReducer(name, reducer) {
|
665 | context.sliceCaseReducersByName[name] = reducer
|
666 | return contextMethods
|
667 | },
|
668 | }
|
669 |
|
670 | reducerNames.forEach((reducerName) => {
|
671 | const reducerDefinition = reducers[reducerName]
|
672 | const reducerDetails: ReducerDetails = {
|
673 | reducerName,
|
674 | type: getType(name, reducerName),
|
675 | createNotation: typeof options.reducers === 'function',
|
676 | }
|
677 | if (isAsyncThunkSliceReducerDefinition<State>(reducerDefinition)) {
|
678 | handleThunkCaseReducerDefinition(
|
679 | reducerDetails,
|
680 | reducerDefinition,
|
681 | contextMethods,
|
682 | cAT,
|
683 | )
|
684 | } else {
|
685 | handleNormalReducerDefinition<State>(
|
686 | reducerDetails,
|
687 | reducerDefinition as any,
|
688 | contextMethods,
|
689 | )
|
690 | }
|
691 | })
|
692 |
|
693 | function buildReducer() {
|
694 | if (process.env.NODE_ENV !== 'production') {
|
695 | if (typeof options.extraReducers === 'object') {
|
696 | throw new Error(
|
697 | "The object notation for `createSlice.extraReducers` has been removed. Please use the 'builder callback' notation instead: https://redux-toolkit.js.org/api/createSlice",
|
698 | )
|
699 | }
|
700 | }
|
701 | const [
|
702 | extraReducers = {},
|
703 | actionMatchers = [],
|
704 | defaultCaseReducer = undefined,
|
705 | ] =
|
706 | typeof options.extraReducers === 'function'
|
707 | ? executeReducerBuilderCallback(options.extraReducers)
|
708 | : [options.extraReducers]
|
709 |
|
710 | const finalCaseReducers = {
|
711 | ...extraReducers,
|
712 | ...context.sliceCaseReducersByType,
|
713 | }
|
714 |
|
715 | return createReducer(options.initialState, (builder) => {
|
716 | for (let key in finalCaseReducers) {
|
717 | builder.addCase(key, finalCaseReducers[key] as CaseReducer<any>)
|
718 | }
|
719 | for (let sM of context.sliceMatchers) {
|
720 | builder.addMatcher(sM.matcher, sM.reducer)
|
721 | }
|
722 | for (let m of actionMatchers) {
|
723 | builder.addMatcher(m.matcher, m.reducer)
|
724 | }
|
725 | if (defaultCaseReducer) {
|
726 | builder.addDefaultCase(defaultCaseReducer)
|
727 | }
|
728 | })
|
729 | }
|
730 |
|
731 | const selectSelf = (state: State) => state
|
732 |
|
733 | const injectedSelectorCache = new Map<
|
734 | boolean,
|
735 | WeakMap<
|
736 | (rootState: any) => State | undefined,
|
737 | Record<string, (rootState: any) => any>
|
738 | >
|
739 | >()
|
740 |
|
741 | let _reducer: ReducerWithInitialState<State>
|
742 |
|
743 | function reducer(state: State | undefined, action: UnknownAction) {
|
744 | if (!_reducer) _reducer = buildReducer()
|
745 |
|
746 | return _reducer(state, action)
|
747 | }
|
748 |
|
749 | function getInitialState() {
|
750 | if (!_reducer) _reducer = buildReducer()
|
751 |
|
752 | return _reducer.getInitialState()
|
753 | }
|
754 |
|
755 | function makeSelectorProps<CurrentReducerPath extends string = ReducerPath>(
|
756 | reducerPath: CurrentReducerPath,
|
757 | injected = false,
|
758 | ): Pick<
|
759 | Slice<State, CaseReducers, Name, CurrentReducerPath, Selectors>,
|
760 | 'getSelectors' | 'selectors' | 'selectSlice' | 'reducerPath'
|
761 | > {
|
762 | function selectSlice(state: { [K in CurrentReducerPath]: State }) {
|
763 | let sliceState = state[reducerPath]
|
764 | if (typeof sliceState === 'undefined') {
|
765 | if (injected) {
|
766 | sliceState = getInitialState()
|
767 | } else if (process.env.NODE_ENV !== 'production') {
|
768 | throw new Error(
|
769 | 'selectSlice returned undefined for an uninjected slice reducer',
|
770 | )
|
771 | }
|
772 | }
|
773 | return sliceState
|
774 | }
|
775 | function getSelectors(
|
776 | selectState: (rootState: any) => State = selectSelf,
|
777 | ) {
|
778 | const selectorCache = emplace(injectedSelectorCache, injected, {
|
779 | insert: () => new WeakMap(),
|
780 | })
|
781 |
|
782 | return emplace(selectorCache, selectState, {
|
783 | insert: () => {
|
784 | const map: Record<string, Selector<any, any>> = {}
|
785 | for (const [name, selector] of Object.entries(
|
786 | options.selectors ?? {},
|
787 | )) {
|
788 | map[name] = wrapSelector(
|
789 | selector,
|
790 | selectState,
|
791 | getInitialState,
|
792 | injected,
|
793 | )
|
794 | }
|
795 | return map
|
796 | },
|
797 | }) as any
|
798 | }
|
799 | return {
|
800 | reducerPath,
|
801 | getSelectors,
|
802 | get selectors() {
|
803 | return getSelectors(selectSlice)
|
804 | },
|
805 | selectSlice,
|
806 | }
|
807 | }
|
808 |
|
809 | const slice: Slice<State, CaseReducers, Name, ReducerPath, Selectors> = {
|
810 | name,
|
811 | reducer,
|
812 | actions: context.actionCreators as any,
|
813 | caseReducers: context.sliceCaseReducersByName as any,
|
814 | getInitialState,
|
815 | ...makeSelectorProps(reducerPath),
|
816 | injectInto(injectable, { reducerPath: pathOpt, ...config } = {}) {
|
817 | const newReducerPath = pathOpt ?? reducerPath
|
818 | injectable.inject({ reducerPath: newReducerPath, reducer }, config)
|
819 | return {
|
820 | ...slice,
|
821 | ...makeSelectorProps(newReducerPath, true),
|
822 | } as any
|
823 | },
|
824 | }
|
825 | return slice
|
826 | }
|
827 | }
|
828 |
|
829 | function wrapSelector<State, NewState, S extends Selector<State>>(
|
830 | selector: S,
|
831 | selectState: Selector<NewState, State>,
|
832 | getInitialState: () => State,
|
833 | injected?: boolean,
|
834 | ) {
|
835 | function wrapper(rootState: NewState, ...args: any[]) {
|
836 | let sliceState = selectState(rootState)
|
837 | if (typeof sliceState === 'undefined') {
|
838 | if (injected) {
|
839 | sliceState = getInitialState()
|
840 | } else if (process.env.NODE_ENV !== 'production') {
|
841 | throw new Error(
|
842 | 'selectState returned undefined for an uninjected slice reducer',
|
843 | )
|
844 | }
|
845 | }
|
846 | return selector(sliceState, ...args)
|
847 | }
|
848 | wrapper.unwrapped = selector
|
849 | return wrapper as RemappedSelector<S, NewState>
|
850 | }
|
851 |
|
852 |
|
853 |
|
854 |
|
855 |
|
856 |
|
857 |
|
858 |
|
859 |
|
860 | export const createSlice = buildCreateSlice()
|
861 |
|
862 | interface ReducerHandlingContext<State> {
|
863 | sliceCaseReducersByName: Record<
|
864 | string,
|
865 | | CaseReducer<State, any>
|
866 | | Pick<
|
867 | AsyncThunkSliceReducerDefinition<State, any, any, any>,
|
868 | 'fulfilled' | 'rejected' | 'pending' | 'settled'
|
869 | >
|
870 | >
|
871 | sliceCaseReducersByType: Record<string, CaseReducer<State, any>>
|
872 | sliceMatchers: ActionMatcherDescriptionCollection<State>
|
873 | actionCreators: Record<string, Function>
|
874 | }
|
875 |
|
876 | interface ReducerHandlingContextMethods<State> {
|
877 | |
878 |
|
879 |
|
880 |
|
881 |
|
882 | addCase<ActionCreator extends TypedActionCreator<string>>(
|
883 | actionCreator: ActionCreator,
|
884 | reducer: CaseReducer<State, ReturnType<ActionCreator>>,
|
885 | ): ReducerHandlingContextMethods<State>
|
886 | |
887 |
|
888 |
|
889 |
|
890 |
|
891 | addCase<Type extends string, A extends Action<Type>>(
|
892 | type: Type,
|
893 | reducer: CaseReducer<State, A>,
|
894 | ): ReducerHandlingContextMethods<State>
|
895 |
|
896 | |
897 |
|
898 |
|
899 |
|
900 |
|
901 |
|
902 |
|
903 |
|
904 |
|
905 |
|
906 |
|
907 | addMatcher<A>(
|
908 | matcher: TypeGuard<A>,
|
909 | reducer: CaseReducer<State, A extends Action ? A : A & Action>,
|
910 | ): ReducerHandlingContextMethods<State>
|
911 | |
912 |
|
913 |
|
914 |
|
915 |
|
916 |
|
917 |
|
918 |
|
919 |
|
920 |
|
921 |
|
922 | exposeAction(
|
923 | name: string,
|
924 | actionCreator: Function,
|
925 | ): ReducerHandlingContextMethods<State>
|
926 | |
927 |
|
928 |
|
929 |
|
930 |
|
931 |
|
932 |
|
933 |
|
934 |
|
935 |
|
936 |
|
937 | exposeCaseReducer(
|
938 | name: string,
|
939 | reducer:
|
940 | | CaseReducer<State, any>
|
941 | | Pick<
|
942 | AsyncThunkSliceReducerDefinition<State, any, any, any>,
|
943 | 'fulfilled' | 'rejected' | 'pending' | 'settled'
|
944 | >,
|
945 | ): ReducerHandlingContextMethods<State>
|
946 | }
|
947 |
|
948 | interface ReducerDetails {
|
949 |
|
950 | reducerName: string
|
951 |
|
952 | type: string
|
953 |
|
954 | createNotation: boolean
|
955 | }
|
956 |
|
957 | function buildReducerCreators<State>(): ReducerCreators<State> {
|
958 | function asyncThunk(
|
959 | payloadCreator: AsyncThunkPayloadCreator<any, any>,
|
960 | config: AsyncThunkSliceReducerConfig<State, any>,
|
961 | ): AsyncThunkSliceReducerDefinition<State, any> {
|
962 | return {
|
963 | _reducerDefinitionType: ReducerType.asyncThunk,
|
964 | payloadCreator,
|
965 | ...config,
|
966 | }
|
967 | }
|
968 | asyncThunk.withTypes = () => asyncThunk
|
969 | return {
|
970 | reducer(caseReducer: CaseReducer<State, any>) {
|
971 | return Object.assign(
|
972 | {
|
973 |
|
974 |
|
975 | [caseReducer.name](...args: Parameters<typeof caseReducer>) {
|
976 | return caseReducer(...args)
|
977 | },
|
978 | }[caseReducer.name],
|
979 | {
|
980 | _reducerDefinitionType: ReducerType.reducer,
|
981 | } as const,
|
982 | )
|
983 | },
|
984 | preparedReducer(prepare, reducer) {
|
985 | return {
|
986 | _reducerDefinitionType: ReducerType.reducerWithPrepare,
|
987 | prepare,
|
988 | reducer,
|
989 | }
|
990 | },
|
991 | asyncThunk: asyncThunk as any,
|
992 | }
|
993 | }
|
994 |
|
995 | function handleNormalReducerDefinition<State>(
|
996 | { type, reducerName, createNotation }: ReducerDetails,
|
997 | maybeReducerWithPrepare:
|
998 | | CaseReducer<State, { payload: any; type: string }>
|
999 | | CaseReducerWithPrepare<State, PayloadAction<any, string, any, any>>,
|
1000 | context: ReducerHandlingContextMethods<State>,
|
1001 | ) {
|
1002 | let caseReducer: CaseReducer<State, any>
|
1003 | let prepareCallback: PrepareAction<any> | undefined
|
1004 | if ('reducer' in maybeReducerWithPrepare) {
|
1005 | if (
|
1006 | createNotation &&
|
1007 | !isCaseReducerWithPrepareDefinition(maybeReducerWithPrepare)
|
1008 | ) {
|
1009 | throw new Error(
|
1010 | 'Please use the `create.preparedReducer` notation for prepared action creators with the `create` notation.',
|
1011 | )
|
1012 | }
|
1013 | caseReducer = maybeReducerWithPrepare.reducer
|
1014 | prepareCallback = maybeReducerWithPrepare.prepare
|
1015 | } else {
|
1016 | caseReducer = maybeReducerWithPrepare
|
1017 | }
|
1018 | context
|
1019 | .addCase(type, caseReducer)
|
1020 | .exposeCaseReducer(reducerName, caseReducer)
|
1021 | .exposeAction(
|
1022 | reducerName,
|
1023 | prepareCallback
|
1024 | ? createAction(type, prepareCallback)
|
1025 | : createAction(type),
|
1026 | )
|
1027 | }
|
1028 |
|
1029 | function isAsyncThunkSliceReducerDefinition<State>(
|
1030 | reducerDefinition: any,
|
1031 | ): reducerDefinition is AsyncThunkSliceReducerDefinition<State, any, any, any> {
|
1032 | return reducerDefinition._reducerDefinitionType === ReducerType.asyncThunk
|
1033 | }
|
1034 |
|
1035 | function isCaseReducerWithPrepareDefinition<State>(
|
1036 | reducerDefinition: any,
|
1037 | ): reducerDefinition is CaseReducerWithPrepareDefinition<State, any> {
|
1038 | return (
|
1039 | reducerDefinition._reducerDefinitionType === ReducerType.reducerWithPrepare
|
1040 | )
|
1041 | }
|
1042 |
|
1043 | function handleThunkCaseReducerDefinition<State>(
|
1044 | { type, reducerName }: ReducerDetails,
|
1045 | reducerDefinition: AsyncThunkSliceReducerDefinition<State, any, any, any>,
|
1046 | context: ReducerHandlingContextMethods<State>,
|
1047 | cAT: typeof _createAsyncThunk | undefined,
|
1048 | ) {
|
1049 | if (!cAT) {
|
1050 | throw new Error(
|
1051 | 'Cannot use `create.asyncThunk` in the built-in `createSlice`. ' +
|
1052 | 'Use `buildCreateSlice({ creators: { asyncThunk: asyncThunkCreator } })` to create a customised version of `createSlice`.',
|
1053 | )
|
1054 | }
|
1055 | const { payloadCreator, fulfilled, pending, rejected, settled, options } =
|
1056 | reducerDefinition
|
1057 | const thunk = cAT(type, payloadCreator, options as any)
|
1058 | context.exposeAction(reducerName, thunk)
|
1059 |
|
1060 | if (fulfilled) {
|
1061 | context.addCase(thunk.fulfilled, fulfilled)
|
1062 | }
|
1063 | if (pending) {
|
1064 | context.addCase(thunk.pending, pending)
|
1065 | }
|
1066 | if (rejected) {
|
1067 | context.addCase(thunk.rejected, rejected)
|
1068 | }
|
1069 | if (settled) {
|
1070 | context.addMatcher(thunk.settled, settled)
|
1071 | }
|
1072 |
|
1073 | context.exposeCaseReducer(reducerName, {
|
1074 | fulfilled: fulfilled || noop,
|
1075 | pending: pending || noop,
|
1076 | rejected: rejected || noop,
|
1077 | settled: settled || noop,
|
1078 | })
|
1079 | }
|
1080 |
|
1081 | function noop() {}
|