1074 lines
32 KiB
TypeScript
1074 lines
32 KiB
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,
|
|
AsyncThunkReducers,
|
|
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 = {},
|
|
> = AsyncThunkReducers<State, ThunkArg, Returned, ThunkApiConfig> & {
|
|
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() {}
|