UNPKG

32.5 kBPlain TextView Raw
1import type { Action, UnknownAction, Reducer } from 'redux'
2import type { Selector } from 'reselect'
3import type {
4 ActionCreatorWithoutPayload,
5 PayloadAction,
6 PayloadActionCreator,
7 PrepareAction,
8 _ActionCreatorWithPreparedPayload,
9} from './createAction'
10import { createAction } from './createAction'
11import type {
12 ActionMatcherDescriptionCollection,
13 CaseReducer,
14 ReducerWithInitialState,
15} from './createReducer'
16import { createReducer } from './createReducer'
17import type { ActionReducerMapBuilder, TypedActionCreator } from './mapBuilders'
18import { executeReducerBuilderCallback } from './mapBuilders'
19import type { Id, Tail, TypeGuard } from './tsHelpers'
20import type { InjectConfig } from './combineSlices'
21import type {
22 AsyncThunk,
23 AsyncThunkConfig,
24 AsyncThunkOptions,
25 AsyncThunkPayloadCreator,
26 OverrideThunkApiConfigs,
27} from './createAsyncThunk'
28import { createAsyncThunk as _createAsyncThunk } from './createAsyncThunk'
29import { emplace } from './utils'
30
31const asyncThunkSymbol = /* @__PURE__ */ Symbol.for(
32 'rtk-slice-createasyncthunk',
33)
34// type is annotated because it's too long to infer
35export const asyncThunkCreator: {
36 [asyncThunkSymbol]: typeof _createAsyncThunk
37} = {
38 [asyncThunkSymbol]: _createAsyncThunk,
39}
40
41interface InjectIntoConfig<NewReducerPath extends string> extends InjectConfig {
42 reducerPath?: NewReducerPath
43}
44
45/**
46 * The return value of `createSlice`
47 *
48 * @public
49 */
50export 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 * The slice name.
59 */
60 name: Name
61
62 /**
63 * The slice reducer path.
64 */
65 reducerPath: ReducerPath
66
67 /**
68 * The slice's reducer.
69 */
70 reducer: Reducer<State>
71
72 /**
73 * Action creators for the types of actions that are handled by the slice
74 * reducer.
75 */
76 actions: CaseReducerActions<CaseReducers, Name>
77
78 /**
79 * The individual case reducer functions that were passed in the `reducers` parameter.
80 * This enables reuse and testing if they were defined inline when calling `createSlice`.
81 */
82 caseReducers: SliceDefinedCaseReducers<CaseReducers>
83
84 /**
85 * Provides access to the initial state value given to the slice.
86 * If a lazy state initializer was provided, it will be called and a fresh value returned.
87 */
88 getInitialState: () => State
89
90 /**
91 * Get localised slice selectors (expects to be called with *just* the slice's state as the first parameter)
92 */
93 getSelectors(): Id<SliceDefinedSelectors<State, Selectors, State>>
94
95 /**
96 * Get globalised slice selectors (`selectState` callback is expected to receive first parameter and return slice state)
97 */
98 getSelectors<RootState>(
99 selectState: (rootState: RootState) => State,
100 ): Id<SliceDefinedSelectors<State, Selectors, RootState>>
101
102 /**
103 * Selectors that assume the slice's state is `rootState[slice.reducerPath]` (which is usually the case)
104 *
105 * Equivalent to `slice.getSelectors((state: RootState) => state[slice.reducerPath])`.
106 */
107 get selectors(): Id<
108 SliceDefinedSelectors<State, Selectors, { [K in ReducerPath]: State }>
109 >
110
111 /**
112 * Inject slice into provided reducer (return value from `combineSlices`), and return injected slice.
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 * Select the slice state, using the slice's current reducerPath.
127 *
128 * Will throw an error if slice is not found.
129 */
130 selectSlice(state: { [K in ReducerPath]: State }): State
131}
132
133/**
134 * A slice after being called with `injectInto(reducer)`.
135 *
136 * Selectors can now be called with an `undefined` value, in which case they use the slice's initial state.
137 */
138interface 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 * Get localised slice selectors (expects to be called with *just* the slice's state as the first parameter)
150 */
151 getSelectors(): Id<SliceDefinedSelectors<State, Selectors, State | undefined>>
152
153 /**
154 * Get globalised slice selectors (`selectState` callback is expected to receive first parameter and return slice state)
155 */
156 getSelectors<RootState>(
157 selectState: (rootState: RootState) => State | undefined,
158 ): Id<SliceDefinedSelectors<State, Selectors, RootState>>
159
160 /**
161 * Selectors that assume the slice's state is `rootState[slice.name]` (which is usually the case)
162 *
163 * Equivalent to `slice.getSelectors((state: RootState) => state[slice.name])`.
164 */
165 get selectors(): Id<
166 SliceDefinedSelectors<
167 State,
168 Selectors,
169 { [K in ReducerPath]?: State | undefined }
170 >
171 >
172
173 /**
174 * Select the slice state, using the slice's current reducerPath.
175 *
176 * Returns initial state if slice is not found.
177 */
178 selectSlice(state: { [K in ReducerPath]?: State | undefined }): State
179}
180
181/**
182 * Options for `createSlice()`.
183 *
184 * @public
185 */
186export 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 * The slice's name. Used to namespace the generated action types.
195 */
196 name: Name
197
198 /**
199 * The slice's reducer path. Used when injecting into a combined slice reducer.
200 */
201 reducerPath?: ReducerPath
202
203 /**
204 * The initial state that should be used when the reducer is called the first time. This may also be a "lazy initializer" function, which should return an initial state value when called. This will be used whenever the reducer is called with `undefined` as its state value, and is primarily useful for cases like reading initial state from `localStorage`.
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
224import { createAction, createSlice, Action } from '@reduxjs/toolkit'
225const incrementBy = createAction<number>('incrementBy')
226const decrement = createAction('decrement')
227
228interface RejectedAction extends Action {
229 error: Error
230}
231
232function isRejectedAction(action: Action): action is RejectedAction {
233 return action.type.endsWith('rejected')
234}
235
236createSlice({
237 name: 'counter',
238 initialState: 0,
239 reducers: {},
240 extraReducers: builder => {
241 builder
242 .addCase(incrementBy, (state, action) => {
243 // action is inferred correctly here if using TS
244 })
245 // You can chain calls, or have separate `builder.addCase()` lines each time
246 .addCase(decrement, (state, action) => {})
247 // You can match a range of action types
248 .addMatcher(
249 isRejectedAction,
250 // `action` will be inferred as a RejectedAction due to isRejectedAction being defined as a type guard
251 (state, action) => {}
252 )
253 // and provide a default case if no other handlers matched
254 .addDefaultCase((state, action) => {})
255 }
256})
257```
258 */
259 extraReducers?: (builder: ActionReducerMapBuilder<State>) => void
260
261 /**
262 * A map of selectors that receive the slice's state and any additional arguments, and return a result.
263 */
264 selectors?: Selectors
265}
266
267export enum ReducerType {
268 reducer = 'reducer',
269 reducerWithPrepare = 'reducerWithPrepare',
270 asyncThunk = 'asyncThunk',
271}
272
273interface ReducerDefinition<T extends ReducerType = ReducerType> {
274 _reducerDefinitionType: T
275}
276
277export interface CaseReducerDefinition<
278 S = any,
279 A extends Action = UnknownAction,
280> extends CaseReducer<S, A>,
281 ReducerDefinition<ReducerType.reducer> {}
282
283/**
284 * A CaseReducer with a `prepare` method.
285 *
286 * @public
287 */
288export type CaseReducerWithPrepare<State, Action extends PayloadAction> = {
289 reducer: CaseReducer<State, Action>
290 prepare: PrepareAction<Action['payload']>
291}
292
293export interface CaseReducerWithPrepareDefinition<
294 State,
295 Action extends PayloadAction,
296> extends CaseReducerWithPrepare<State, Action>,
297 ReducerDefinition<ReducerType.reducerWithPrepare> {}
298
299export 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
326export 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 * Providing these as part of the config would cause circular types, so we disallow passing them
343 */
344type PreventCircular<ThunkApiConfig> = {
345 [K in keyof ThunkApiConfig]: K extends 'state' | 'dispatch'
346 ? never
347 : ThunkApiConfig[K]
348}
349
350interface 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
398export 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 * The type describing a slice's `reducers` option.
426 *
427 * @public
428 */
429export 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 * The type describing a slice's `selectors` option.
439 */
440export type SliceSelectors<State> = {
441 [K: string]: (sliceState: State, ...args: any[]) => any
442}
443
444type SliceActionType<
445 SliceName extends string,
446 ActionName extends keyof any,
447> = ActionName extends string | number ? `${SliceName}/${ActionName}` : string
448
449/**
450 * Derives the slice's `actions` property from the `reducers` options
451 *
452 * @public
453 */
454export 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 * Get a `PayloadActionCreator` type for a passed `CaseReducerWithPrepare`
485 *
486 * @internal
487 */
488type ActionCreatorForCaseReducerWithPrepare<
489 CR extends { prepare: any },
490 Type extends string,
491> = _ActionCreatorWithPreparedPayload<CR['prepare'], Type>
492
493/**
494 * Get a `PayloadActionCreator` type for a passed `CaseReducer`
495 *
496 * @internal
497 */
498type 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 * Extracts the CaseReducers out of a `reducers` object, even if they are
509 * tested into a `CaseReducerWithPrepare`.
510 *
511 * @internal
512 */
513type 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
530type 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 * Extracts the final selector type from the `selectors` object.
537 *
538 * Removes the `string` index signature from the default value.
539 */
540type 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 * Used on a SliceCaseReducers object.
553 * Ensures that if a CaseReducer is a `CaseReducerWithPrepare`, that
554 * the `reducer` and the `prepare` function use the same type of `payload`.
555 *
556 * Might do additional such checks in the future.
557 *
558 * This type is only ever useful if you want to write your own wrapper around
559 * `createSlice`. Please don't use it otherwise!
560 *
561 * @public
562 */
563export 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
576function getType(slice: string, actionKey: string): string {
577 return `${slice}/${actionKey}`
578}
579
580interface BuildCreateSliceConfig {
581 creators?: {
582 asyncThunk?: typeof asyncThunkCreator
583 }
584}
585
586export 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
829function 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 * A function that accepts an initial state, an object full of reducer
854 * functions, and a "slice name", and automatically generates
855 * action creators and action types that correspond to the
856 * reducers and state.
857 *
858 * @public
859 */
860export const createSlice = /* @__PURE__ */ buildCreateSlice()
861
862interface 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
876interface ReducerHandlingContextMethods<State> {
877 /**
878 * Adds a case reducer to handle a single action type.
879 * @param actionCreator - Either a plain action type string, or an action creator generated by [`createAction`](./createAction) that can be used to determine the action type.
880 * @param reducer - The actual case reducer function.
881 */
882 addCase<ActionCreator extends TypedActionCreator<string>>(
883 actionCreator: ActionCreator,
884 reducer: CaseReducer<State, ReturnType<ActionCreator>>,
885 ): ReducerHandlingContextMethods<State>
886 /**
887 * Adds a case reducer to handle a single action type.
888 * @param actionCreator - Either a plain action type string, or an action creator generated by [`createAction`](./createAction) that can be used to determine the action type.
889 * @param reducer - The actual case reducer function.
890 */
891 addCase<Type extends string, A extends Action<Type>>(
892 type: Type,
893 reducer: CaseReducer<State, A>,
894 ): ReducerHandlingContextMethods<State>
895
896 /**
897 * Allows you to match incoming actions against your own filter function instead of only the `action.type` property.
898 * @remarks
899 * If multiple matcher reducers match, all of them will be executed in the order
900 * they were defined in - even if a case reducer already matched.
901 * All calls to `builder.addMatcher` must come after any calls to `builder.addCase` and before any calls to `builder.addDefaultCase`.
902 * @param matcher - A matcher function. In TypeScript, this should be a [type predicate](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates)
903 * function
904 * @param reducer - The actual case reducer function.
905 *
906 */
907 addMatcher<A>(
908 matcher: TypeGuard<A>,
909 reducer: CaseReducer<State, A extends Action ? A : A & Action>,
910 ): ReducerHandlingContextMethods<State>
911 /**
912 * Add an action to be exposed under the final `slice.actions` key.
913 * @param name The key to be exposed as.
914 * @param actionCreator The action to expose.
915 * @example
916 * context.exposeAction("addPost", createAction<Post>("addPost"));
917 *
918 * export const { addPost } = slice.actions
919 *
920 * dispatch(addPost(post))
921 */
922 exposeAction(
923 name: string,
924 actionCreator: Function,
925 ): ReducerHandlingContextMethods<State>
926 /**
927 * Add a case reducer to be exposed under the final `slice.caseReducers` key.
928 * @param name The key to be exposed as.
929 * @param reducer The reducer to expose.
930 * @example
931 * context.exposeCaseReducer("addPost", (state, action: PayloadAction<Post>) => {
932 * state.push(action.payload)
933 * })
934 *
935 * slice.caseReducers.addPost([], addPost(post))
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
948interface ReducerDetails {
949 /** The key the reducer was defined under */
950 reducerName: string
951 /** The predefined action type, i.e. `${slice.name}/${reducerName}` */
952 type: string
953 /** Whether create. notation was used when defining reducers */
954 createNotation: boolean
955}
956
957function 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 // hack so the wrapping function has the same name as the original
974 // we need to create a wrapper so the `reducerDefinitionType` is not assigned to the original
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
995function 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
1029function isAsyncThunkSliceReducerDefinition<State>(
1030 reducerDefinition: any,
1031): reducerDefinition is AsyncThunkSliceReducerDefinition<State, any, any, any> {
1032 return reducerDefinition._reducerDefinitionType === ReducerType.asyncThunk
1033}
1034
1035function isCaseReducerWithPrepareDefinition<State>(
1036 reducerDefinition: any,
1037): reducerDefinition is CaseReducerWithPrepareDefinition<State, any> {
1038 return (
1039 reducerDefinition._reducerDefinitionType === ReducerType.reducerWithPrepare
1040 )
1041}
1042
1043function 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
1081function noop() {}