UNPKG

@reduxjs/toolkit

Version:

The official, opinionated, batteries-included toolset for efficient Redux development

1,088 lines (1,001 loc) 32.7 kB
import type { Action, Reducer, UnknownAction } from 'redux' import type { Selector } from 'reselect' import type { InjectConfig } from './combineSlices' import type { ActionCreatorWithoutPayload, PayloadAction, PayloadActionCreator, PrepareAction, _ActionCreatorWithPreparedPayload, } from './createAction' import { createAction } from './createAction' import type { AsyncThunk, AsyncThunkConfig, AsyncThunkOptions, AsyncThunkPayloadCreator, OverrideThunkApiConfigs, } from './createAsyncThunk' import { createAsyncThunk as _createAsyncThunk } from './createAsyncThunk' import type { ActionMatcherDescriptionCollection, CaseReducer, ReducerWithInitialState, } from './createReducer' import { createReducer } from './createReducer' import type { ActionReducerMapBuilder, TypedActionCreator } from './mapBuilders' import { executeReducerBuilderCallback } from './mapBuilders' import type { Id, TypeGuard } from './tsHelpers' import { getOrInsertComputed } from './utils' const asyncThunkSymbol = /* @__PURE__ */ Symbol.for( 'rtk-slice-createasyncthunk', ) // type is annotated because it's too long to infer export const asyncThunkCreator: { [asyncThunkSymbol]: typeof _createAsyncThunk } = { [asyncThunkSymbol]: _createAsyncThunk, } type InjectIntoConfig<NewReducerPath extends string> = InjectConfig & { reducerPath?: NewReducerPath } /** * The return value of `createSlice` * * @public */ export interface Slice< State = any, CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>, Name extends string = string, ReducerPath extends string = Name, Selectors extends SliceSelectors<State> = SliceSelectors<State>, > { /** * The slice name. */ name: Name /** * The slice reducer path. */ reducerPath: ReducerPath /** * The slice's reducer. */ reducer: Reducer<State> /** * Action creators for the types of actions that are handled by the slice * reducer. */ actions: CaseReducerActions<CaseReducers, Name> /** * The individual case reducer functions that were passed in the `reducers` parameter. * This enables reuse and testing if they were defined inline when calling `createSlice`. */ caseReducers: SliceDefinedCaseReducers<CaseReducers> /** * Provides access to the initial state value given to the slice. * If a lazy state initializer was provided, it will be called and a fresh value returned. */ getInitialState: () => State /** * Get localised slice selectors (expects to be called with *just* the slice's state as the first parameter) */ getSelectors(): Id<SliceDefinedSelectors<State, Selectors, State>> /** * Get globalised slice selectors (`selectState` callback is expected to receive first parameter and return slice state) */ getSelectors<RootState>( selectState: (rootState: RootState) => State, ): Id<SliceDefinedSelectors<State, Selectors, RootState>> /** * Selectors that assume the slice's state is `rootState[slice.reducerPath]` (which is usually the case) * * Equivalent to `slice.getSelectors((state: RootState) => state[slice.reducerPath])`. */ get selectors(): Id< SliceDefinedSelectors<State, Selectors, { [K in ReducerPath]: State }> > /** * Inject slice into provided reducer (return value from `combineSlices`), and return injected slice. */ injectInto<NewReducerPath extends string = ReducerPath>( this: this, injectable: { inject: ( slice: { reducerPath: string; reducer: Reducer }, config?: InjectConfig, ) => void }, config?: InjectIntoConfig<NewReducerPath>, ): InjectedSlice<State, CaseReducers, Name, NewReducerPath, Selectors> /** * Select the slice state, using the slice's current reducerPath. * * Will throw an error if slice is not found. */ selectSlice(state: { [K in ReducerPath]: State }): State } /** * A slice after being called with `injectInto(reducer)`. * * Selectors can now be called with an `undefined` value, in which case they use the slice's initial state. */ type InjectedSlice< State = any, CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>, Name extends string = string, ReducerPath extends string = Name, Selectors extends SliceSelectors<State> = SliceSelectors<State>, > = Omit< Slice<State, CaseReducers, Name, ReducerPath, Selectors>, 'getSelectors' | 'selectors' > & { /** * Get localised slice selectors (expects to be called with *just* the slice's state as the first parameter) */ getSelectors(): Id<SliceDefinedSelectors<State, Selectors, State | undefined>> /** * Get globalised slice selectors (`selectState` callback is expected to receive first parameter and return slice state) */ getSelectors<RootState>( selectState: (rootState: RootState) => State | undefined, ): Id<SliceDefinedSelectors<State, Selectors, RootState>> /** * Selectors that assume the slice's state is `rootState[slice.name]` (which is usually the case) * * Equivalent to `slice.getSelectors((state: RootState) => state[slice.name])`. */ get selectors(): Id< SliceDefinedSelectors< State, Selectors, { [K in ReducerPath]?: State | undefined } > > /** * Select the slice state, using the slice's current reducerPath. * * Returns initial state if slice is not found. */ selectSlice(state: { [K in ReducerPath]?: State | undefined }): State } /** * Options for `createSlice()`. * * @public */ export interface CreateSliceOptions< State = any, CR extends SliceCaseReducers<State> = SliceCaseReducers<State>, Name extends string = string, ReducerPath extends string = Name, Selectors extends SliceSelectors<State> = SliceSelectors<State>, > { /** * The slice's name. Used to namespace the generated action types. */ name: Name /** * The slice's reducer path. Used when injecting into a combined slice reducer. */ reducerPath?: ReducerPath /** * 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`. */ initialState: State | (() => State) /** * A mapping from action types to action-type-specific *case reducer* * functions. For every action type, a matching action creator will be * generated using `createAction()`. */ reducers: | ValidateSliceCaseReducers<State, CR> | ((creators: ReducerCreators<State>) => CR) /** * A callback that receives a *builder* object to define * case reducers via calls to `builder.addCase(actionCreatorOrType, reducer)`. * * * @example ```ts import { createAction, createSlice, Action } from '@reduxjs/toolkit' const incrementBy = createAction<number>('incrementBy') const decrement = createAction('decrement') interface RejectedAction extends Action { error: Error } function isRejectedAction(action: Action): action is RejectedAction { return action.type.endsWith('rejected') } createSlice({ name: 'counter', initialState: 0, reducers: {}, extraReducers: builder => { builder .addCase(incrementBy, (state, action) => { // action is inferred correctly here if using TS }) // You can chain calls, or have separate `builder.addCase()` lines each time .addCase(decrement, (state, action) => {}) // You can match a range of action types .addMatcher( isRejectedAction, // `action` will be inferred as a RejectedAction due to isRejectedAction being defined as a type guard (state, action) => {} ) // and provide a default case if no other handlers matched .addDefaultCase((state, action) => {}) } }) ``` */ extraReducers?: (builder: ActionReducerMapBuilder<State>) => void /** * A map of selectors that receive the slice's state and any additional arguments, and return a result. */ selectors?: Selectors } export enum ReducerType { reducer = 'reducer', reducerWithPrepare = 'reducerWithPrepare', asyncThunk = 'asyncThunk', } type ReducerDefinition<T extends ReducerType = ReducerType> = { _reducerDefinitionType: T } export type CaseReducerDefinition< S = any, A extends Action = UnknownAction, > = CaseReducer<S, A> & ReducerDefinition<ReducerType.reducer> /** * A CaseReducer with a `prepare` method. * * @public */ export type CaseReducerWithPrepare<State, Action extends PayloadAction> = { reducer: CaseReducer<State, Action> prepare: PrepareAction<Action['payload']> } export interface CaseReducerWithPrepareDefinition< State, Action extends PayloadAction, > extends CaseReducerWithPrepare<State, Action>, ReducerDefinition<ReducerType.reducerWithPrepare> {} type AsyncThunkSliceReducerConfig< State, ThunkArg extends any, Returned = unknown, ThunkApiConfig extends AsyncThunkConfig = {}, > = { pending?: CaseReducer< State, ReturnType<AsyncThunk<Returned, ThunkArg, ThunkApiConfig>['pending']> > rejected?: CaseReducer< State, ReturnType<AsyncThunk<Returned, ThunkArg, ThunkApiConfig>['rejected']> > fulfilled?: CaseReducer< State, ReturnType<AsyncThunk<Returned, ThunkArg, ThunkApiConfig>['fulfilled']> > settled?: CaseReducer< State, ReturnType< AsyncThunk<Returned, ThunkArg, ThunkApiConfig>['rejected' | 'fulfilled'] > > options?: AsyncThunkOptions<ThunkArg, ThunkApiConfig> } type AsyncThunkSliceReducerDefinition< State, ThunkArg extends any, Returned = unknown, ThunkApiConfig extends AsyncThunkConfig = {}, > = AsyncThunkSliceReducerConfig<State, ThunkArg, Returned, ThunkApiConfig> & ReducerDefinition<ReducerType.asyncThunk> & { payloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg, ThunkApiConfig> } /** * Providing these as part of the config would cause circular types, so we disallow passing them */ type PreventCircular<ThunkApiConfig> = { [K in keyof ThunkApiConfig]: K extends 'state' | 'dispatch' ? never : ThunkApiConfig[K] } interface AsyncThunkCreator< State, CurriedThunkApiConfig extends PreventCircular<AsyncThunkConfig> = PreventCircular<AsyncThunkConfig>, > { <Returned, ThunkArg = void>( payloadCreator: AsyncThunkPayloadCreator< Returned, ThunkArg, CurriedThunkApiConfig >, config?: AsyncThunkSliceReducerConfig< State, ThunkArg, Returned, CurriedThunkApiConfig >, ): AsyncThunkSliceReducerDefinition< State, ThunkArg, Returned, CurriedThunkApiConfig > < Returned, ThunkArg, ThunkApiConfig extends PreventCircular<AsyncThunkConfig> = {}, >( payloadCreator: AsyncThunkPayloadCreator< Returned, ThunkArg, ThunkApiConfig >, config?: AsyncThunkSliceReducerConfig< State, ThunkArg, Returned, ThunkApiConfig >, ): AsyncThunkSliceReducerDefinition<State, ThunkArg, Returned, ThunkApiConfig> withTypes< ThunkApiConfig extends PreventCircular<AsyncThunkConfig>, >(): AsyncThunkCreator< State, OverrideThunkApiConfigs<CurriedThunkApiConfig, ThunkApiConfig> > } export interface ReducerCreators<State> { reducer( caseReducer: CaseReducer<State, PayloadAction>, ): CaseReducerDefinition<State, PayloadAction> reducer<Payload>( caseReducer: CaseReducer<State, PayloadAction<Payload>>, ): CaseReducerDefinition<State, PayloadAction<Payload>> asyncThunk: AsyncThunkCreator<State> preparedReducer<Prepare extends PrepareAction<any>>( prepare: Prepare, reducer: CaseReducer< State, ReturnType<_ActionCreatorWithPreparedPayload<Prepare>> >, ): { _reducerDefinitionType: ReducerType.reducerWithPrepare prepare: Prepare reducer: CaseReducer< State, ReturnType<_ActionCreatorWithPreparedPayload<Prepare>> > } } /** * The type describing a slice's `reducers` option. * * @public */ export type SliceCaseReducers<State> = | Record<string, ReducerDefinition> | Record< string, | CaseReducer<State, PayloadAction<any>> | CaseReducerWithPrepare<State, PayloadAction<any, string, any, any>> > /** * The type describing a slice's `selectors` option. */ export type SliceSelectors<State> = { [K: string]: (sliceState: State, ...args: any[]) => any } type SliceActionType< SliceName extends string, ActionName extends keyof any, > = ActionName extends string | number ? `${SliceName}/${ActionName}` : string /** * Derives the slice's `actions` property from the `reducers` options * * @public */ export type CaseReducerActions< CaseReducers extends SliceCaseReducers<any>, SliceName extends string, > = { [Type in keyof CaseReducers]: CaseReducers[Type] extends infer Definition ? Definition extends { prepare: any } ? ActionCreatorForCaseReducerWithPrepare< Definition, SliceActionType<SliceName, Type> > : Definition extends AsyncThunkSliceReducerDefinition< any, infer ThunkArg, infer Returned, infer ThunkApiConfig > ? AsyncThunk<Returned, ThunkArg, ThunkApiConfig> : Definition extends { reducer: any } ? ActionCreatorForCaseReducer< Definition['reducer'], SliceActionType<SliceName, Type> > : ActionCreatorForCaseReducer< Definition, SliceActionType<SliceName, Type> > : never } /** * Get a `PayloadActionCreator` type for a passed `CaseReducerWithPrepare` * * @internal */ type ActionCreatorForCaseReducerWithPrepare< CR extends { prepare: any }, Type extends string, > = _ActionCreatorWithPreparedPayload<CR['prepare'], Type> /** * Get a `PayloadActionCreator` type for a passed `CaseReducer` * * @internal */ type ActionCreatorForCaseReducer<CR, Type extends string> = CR extends ( state: any, action: infer Action, ) => any ? Action extends { payload: infer P } ? PayloadActionCreator<P, Type> : ActionCreatorWithoutPayload<Type> : ActionCreatorWithoutPayload<Type> /** * Extracts the CaseReducers out of a `reducers` object, even if they are * tested into a `CaseReducerWithPrepare`. * * @internal */ type SliceDefinedCaseReducers<CaseReducers extends SliceCaseReducers<any>> = { [Type in keyof CaseReducers]: CaseReducers[Type] extends infer Definition ? Definition extends AsyncThunkSliceReducerDefinition<any, any, any> ? Id< Pick< Required<Definition>, 'fulfilled' | 'rejected' | 'pending' | 'settled' > > : Definition extends { reducer: infer Reducer } ? Reducer : Definition : never } type RemappedSelector<S extends Selector, NewState> = S extends Selector<any, infer R, infer P> ? Selector<NewState, R, P> & { unwrapped: S } : never /** * Extracts the final selector type from the `selectors` object. * * Removes the `string` index signature from the default value. */ type SliceDefinedSelectors< State, Selectors extends SliceSelectors<State>, RootState, > = { [K in keyof Selectors as string extends K ? never : K]: RemappedSelector< Selectors[K], RootState > } /** * Used on a SliceCaseReducers object. * Ensures that if a CaseReducer is a `CaseReducerWithPrepare`, that * the `reducer` and the `prepare` function use the same type of `payload`. * * Might do additional such checks in the future. * * This type is only ever useful if you want to write your own wrapper around * `createSlice`. Please don't use it otherwise! * * @public */ export type ValidateSliceCaseReducers< S, ACR extends SliceCaseReducers<S>, > = ACR & { [T in keyof ACR]: ACR[T] extends { reducer(s: S, action?: infer A): any } ? { prepare(...a: never[]): Omit<A, 'type'> } : {} } function getType(slice: string, actionKey: string): string { return `${slice}/${actionKey}` } interface BuildCreateSliceConfig { creators?: { asyncThunk?: typeof asyncThunkCreator } } export function buildCreateSlice({ creators }: BuildCreateSliceConfig = {}) { const cAT = creators?.asyncThunk?.[asyncThunkSymbol] return function createSlice< State, CaseReducers extends SliceCaseReducers<State>, Name extends string, Selectors extends SliceSelectors<State>, ReducerPath extends string = Name, >( options: CreateSliceOptions< State, CaseReducers, Name, ReducerPath, Selectors >, ): Slice<State, CaseReducers, Name, ReducerPath, Selectors> { const { name, reducerPath = name as unknown as ReducerPath } = options if (!name) { throw new Error('`name` is a required option for createSlice') } if ( typeof process !== 'undefined' && process.env.NODE_ENV === 'development' ) { if (options.initialState === undefined) { console.error( 'You must provide an `initialState` value that is not `undefined`. You may have misspelled `initialState`', ) } } const reducers = (typeof options.reducers === 'function' ? options.reducers(buildReducerCreators<State>()) : options.reducers) || {} const reducerNames = Object.keys(reducers) const context: ReducerHandlingContext<State> = { sliceCaseReducersByName: {}, sliceCaseReducersByType: {}, actionCreators: {}, sliceMatchers: [], } const contextMethods: ReducerHandlingContextMethods<State> = { addCase( typeOrActionCreator: string | TypedActionCreator<any>, reducer: CaseReducer<State>, ) { const type = typeof typeOrActionCreator === 'string' ? typeOrActionCreator : typeOrActionCreator.type if (!type) { throw new Error( '`context.addCase` cannot be called with an empty action type', ) } if (type in context.sliceCaseReducersByType) { throw new Error( '`context.addCase` cannot be called with two reducers for the same action type: ' + type, ) } context.sliceCaseReducersByType[type] = reducer return contextMethods }, addMatcher(matcher, reducer) { context.sliceMatchers.push({ matcher, reducer }) return contextMethods }, exposeAction(name, actionCreator) { context.actionCreators[name] = actionCreator return contextMethods }, exposeCaseReducer(name, reducer) { context.sliceCaseReducersByName[name] = reducer return contextMethods }, } reducerNames.forEach((reducerName) => { const reducerDefinition = reducers[reducerName] const reducerDetails: ReducerDetails = { reducerName, type: getType(name, reducerName), createNotation: typeof options.reducers === 'function', } if (isAsyncThunkSliceReducerDefinition<State>(reducerDefinition)) { handleThunkCaseReducerDefinition( reducerDetails, reducerDefinition, contextMethods, cAT, ) } else { handleNormalReducerDefinition<State>( reducerDetails, reducerDefinition as any, contextMethods, ) } }) function buildReducer() { if (process.env.NODE_ENV !== 'production') { if (typeof options.extraReducers === 'object') { throw new Error( "The object notation for `createSlice.extraReducers` has been removed. Please use the 'builder callback' notation instead: https://redux-toolkit.js.org/api/createSlice", ) } } const [ extraReducers = {}, actionMatchers = [], defaultCaseReducer = undefined, ] = typeof options.extraReducers === 'function' ? executeReducerBuilderCallback(options.extraReducers) : [options.extraReducers] const finalCaseReducers = { ...extraReducers, ...context.sliceCaseReducersByType, } return createReducer(options.initialState, (builder) => { for (let key in finalCaseReducers) { builder.addCase(key, finalCaseReducers[key] as CaseReducer<any>) } for (let sM of context.sliceMatchers) { builder.addMatcher(sM.matcher, sM.reducer) } for (let m of actionMatchers) { builder.addMatcher(m.matcher, m.reducer) } if (defaultCaseReducer) { builder.addDefaultCase(defaultCaseReducer) } }) } const selectSelf = (state: State) => state const injectedSelectorCache = new Map< boolean, WeakMap< (rootState: any) => State | undefined, Record<string, (rootState: any) => any> > >() const injectedStateCache = new WeakMap<(rootState: any) => State, State>() let _reducer: ReducerWithInitialState<State> function reducer(state: State | undefined, action: UnknownAction) { if (!_reducer) _reducer = buildReducer() return _reducer(state, action) } function getInitialState() { if (!_reducer) _reducer = buildReducer() return _reducer.getInitialState() } function makeSelectorProps<CurrentReducerPath extends string = ReducerPath>( reducerPath: CurrentReducerPath, injected = false, ): Pick< Slice<State, CaseReducers, Name, CurrentReducerPath, Selectors>, 'getSelectors' | 'selectors' | 'selectSlice' | 'reducerPath' > { function selectSlice(state: { [K in CurrentReducerPath]: State }) { let sliceState = state[reducerPath] if (typeof sliceState === 'undefined') { if (injected) { sliceState = getOrInsertComputed( injectedStateCache, selectSlice, getInitialState, ) } else if (process.env.NODE_ENV !== 'production') { throw new Error( 'selectSlice returned undefined for an uninjected slice reducer', ) } } return sliceState } function getSelectors( selectState: (rootState: any) => State = selectSelf, ) { const selectorCache = getOrInsertComputed( injectedSelectorCache, injected, () => new WeakMap(), ) return getOrInsertComputed(selectorCache, selectState, () => { const map: Record<string, Selector<any, any>> = {} for (const [name, selector] of Object.entries( options.selectors ?? {}, )) { map[name] = wrapSelector( selector, selectState, () => getOrInsertComputed( injectedStateCache, selectState, getInitialState, ), injected, ) } return map }) as any } return { reducerPath, getSelectors, get selectors() { return getSelectors(selectSlice) }, selectSlice, } } const slice: Slice<State, CaseReducers, Name, ReducerPath, Selectors> = { name, reducer, actions: context.actionCreators as any, caseReducers: context.sliceCaseReducersByName as any, getInitialState, ...makeSelectorProps(reducerPath), injectInto(injectable, { reducerPath: pathOpt, ...config } = {}) { const newReducerPath = pathOpt ?? reducerPath injectable.inject({ reducerPath: newReducerPath, reducer }, config) return { ...slice, ...makeSelectorProps(newReducerPath, true), } as any }, } return slice } } function wrapSelector<State, NewState, S extends Selector<State>>( selector: S, selectState: Selector<NewState, State>, getInitialState: () => State, injected?: boolean, ) { function wrapper(rootState: NewState, ...args: any[]) { let sliceState = selectState(rootState) if (typeof sliceState === 'undefined') { if (injected) { sliceState = getInitialState() } else if (process.env.NODE_ENV !== 'production') { throw new Error( 'selectState returned undefined for an uninjected slice reducer', ) } } return selector(sliceState, ...args) } wrapper.unwrapped = selector return wrapper as RemappedSelector<S, NewState> } /** * A function that accepts an initial state, an object full of reducer * functions, and a "slice name", and automatically generates * action creators and action types that correspond to the * reducers and state. * * @public */ export const createSlice = /* @__PURE__ */ buildCreateSlice() interface ReducerHandlingContext<State> { sliceCaseReducersByName: Record< string, | CaseReducer<State, any> | Pick< AsyncThunkSliceReducerDefinition<State, any, any, any>, 'fulfilled' | 'rejected' | 'pending' | 'settled' > > sliceCaseReducersByType: Record<string, CaseReducer<State, any>> sliceMatchers: ActionMatcherDescriptionCollection<State> actionCreators: Record<string, Function> } interface ReducerHandlingContextMethods<State> { /** * Adds a case reducer to handle a single action type. * @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. * @param reducer - The actual case reducer function. */ addCase<ActionCreator extends TypedActionCreator<string>>( actionCreator: ActionCreator, reducer: CaseReducer<State, ReturnType<ActionCreator>>, ): ReducerHandlingContextMethods<State> /** * Adds a case reducer to handle a single action type. * @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. * @param reducer - The actual case reducer function. */ addCase<Type extends string, A extends Action<Type>>( type: Type, reducer: CaseReducer<State, A>, ): ReducerHandlingContextMethods<State> /** * Allows you to match incoming actions against your own filter function instead of only the `action.type` property. * @remarks * If multiple matcher reducers match, all of them will be executed in the order * they were defined in - even if a case reducer already matched. * All calls to `builder.addMatcher` must come after any calls to `builder.addCase` and before any calls to `builder.addDefaultCase`. * @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) * function * @param reducer - The actual case reducer function. * */ addMatcher<A>( matcher: TypeGuard<A>, reducer: CaseReducer<State, A extends Action ? A : A & Action>, ): ReducerHandlingContextMethods<State> /** * Add an action to be exposed under the final `slice.actions` key. * @param name The key to be exposed as. * @param actionCreator The action to expose. * @example * context.exposeAction("addPost", createAction<Post>("addPost")); * * export const { addPost } = slice.actions * * dispatch(addPost(post)) */ exposeAction( name: string, actionCreator: Function, ): ReducerHandlingContextMethods<State> /** * Add a case reducer to be exposed under the final `slice.caseReducers` key. * @param name The key to be exposed as. * @param reducer The reducer to expose. * @example * context.exposeCaseReducer("addPost", (state, action: PayloadAction<Post>) => { * state.push(action.payload) * }) * * slice.caseReducers.addPost([], addPost(post)) */ exposeCaseReducer( name: string, reducer: | CaseReducer<State, any> | Pick< AsyncThunkSliceReducerDefinition<State, any, any, any>, 'fulfilled' | 'rejected' | 'pending' | 'settled' >, ): ReducerHandlingContextMethods<State> } interface ReducerDetails { /** The key the reducer was defined under */ reducerName: string /** The predefined action type, i.e. `${slice.name}/${reducerName}` */ type: string /** Whether create. notation was used when defining reducers */ createNotation: boolean } function buildReducerCreators<State>(): ReducerCreators<State> { function asyncThunk( payloadCreator: AsyncThunkPayloadCreator<any, any>, config: AsyncThunkSliceReducerConfig<State, any>, ): AsyncThunkSliceReducerDefinition<State, any> { return { _reducerDefinitionType: ReducerType.asyncThunk, payloadCreator, ...config, } } asyncThunk.withTypes = () => asyncThunk return { reducer(caseReducer: CaseReducer<State, any>) { return Object.assign( { // hack so the wrapping function has the same name as the original // we need to create a wrapper so the `reducerDefinitionType` is not assigned to the original [caseReducer.name](...args: Parameters<typeof caseReducer>) { return caseReducer(...args) }, }[caseReducer.name], { _reducerDefinitionType: ReducerType.reducer, } as const, ) }, preparedReducer(prepare, reducer) { return { _reducerDefinitionType: ReducerType.reducerWithPrepare, prepare, reducer, } }, asyncThunk: asyncThunk as any, } } function handleNormalReducerDefinition<State>( { type, reducerName, createNotation }: ReducerDetails, maybeReducerWithPrepare: | CaseReducer<State, { payload: any; type: string }> | CaseReducerWithPrepare<State, PayloadAction<any, string, any, any>>, context: ReducerHandlingContextMethods<State>, ) { let caseReducer: CaseReducer<State, any> let prepareCallback: PrepareAction<any> | undefined if ('reducer' in maybeReducerWithPrepare) { if ( createNotation && !isCaseReducerWithPrepareDefinition(maybeReducerWithPrepare) ) { throw new Error( 'Please use the `create.preparedReducer` notation for prepared action creators with the `create` notation.', ) } caseReducer = maybeReducerWithPrepare.reducer prepareCallback = maybeReducerWithPrepare.prepare } else { caseReducer = maybeReducerWithPrepare } context .addCase(type, caseReducer) .exposeCaseReducer(reducerName, caseReducer) .exposeAction( reducerName, prepareCallback ? createAction(type, prepareCallback) : createAction(type), ) } function isAsyncThunkSliceReducerDefinition<State>( reducerDefinition: any, ): reducerDefinition is AsyncThunkSliceReducerDefinition<State, any, any, any> { return reducerDefinition._reducerDefinitionType === ReducerType.asyncThunk } function isCaseReducerWithPrepareDefinition<State>( reducerDefinition: any, ): reducerDefinition is CaseReducerWithPrepareDefinition<State, any> { return ( reducerDefinition._reducerDefinitionType === ReducerType.reducerWithPrepare ) } function handleThunkCaseReducerDefinition<State>( { type, reducerName }: ReducerDetails, reducerDefinition: AsyncThunkSliceReducerDefinition<State, any, any, any>, context: ReducerHandlingContextMethods<State>, cAT: typeof _createAsyncThunk | undefined, ) { if (!cAT) { throw new Error( 'Cannot use `create.asyncThunk` in the built-in `createSlice`. ' + 'Use `buildCreateSlice({ creators: { asyncThunk: asyncThunkCreator } })` to create a customised version of `createSlice`.', ) } const { payloadCreator, fulfilled, pending, rejected, settled, options } = reducerDefinition const thunk = cAT(type, payloadCreator, options as any) context.exposeAction(reducerName, thunk) if (fulfilled) { context.addCase(thunk.fulfilled, fulfilled) } if (pending) { context.addCase(thunk.pending, pending) } if (rejected) { context.addCase(thunk.rejected, rejected) } if (settled) { context.addMatcher(thunk.settled, settled) } context.exposeCaseReducer(reducerName, { fulfilled: fulfilled || noop, pending: pending || noop, rejected: rejected || noop, settled: settled || noop, }) } function noop() {}