source: node_modules/@reduxjs/toolkit/src/createSlice.ts@ a762898

Last change on this file since a762898 was a762898, checked in by istevanoska <ilinastevanoska@…>, 5 months ago

Added visualizations

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