@reduxjs/toolkit
Version:
The official, opinionated, batteries-included toolset for efficient Redux development
1,088 lines (1,001 loc) • 32.7 kB
text/typescript
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() {}