Skip to content

Commit 8f6cea6

Browse files
committed
Create standardised methods of modifying reducer handler context.
1 parent 1890c54 commit 8f6cea6

File tree

1 file changed

+132
-18
lines changed

1 file changed

+132
-18
lines changed

packages/toolkit/src/createSlice.ts

Lines changed: 132 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ import type {
1313
ReducerWithInitialState,
1414
} from './createReducer'
1515
import { createReducer } from './createReducer'
16-
import type { ActionReducerMapBuilder } from './mapBuilders'
16+
import type { ActionReducerMapBuilder, TypedActionCreator } from './mapBuilders'
1717
import { executeReducerBuilderCallback } from './mapBuilders'
18-
import type { Id, Tail } from './tsHelpers'
18+
import type { Id, Tail, TypeGuard } from './tsHelpers'
1919
import type { InjectConfig } from './combineSlices'
2020
import type {
2121
AsyncThunk,
@@ -630,6 +630,43 @@ export function buildCreateSlice({ creators }: BuildCreateSliceConfig = {}) {
630630
sliceMatchers: [],
631631
}
632632

633+
const contextMethods: ReducerHandlingContextMethods<State> = {
634+
addCase(
635+
typeOrActionCreator: string | TypedActionCreator<any>,
636+
reducer: CaseReducer<State>
637+
) {
638+
const type =
639+
typeof typeOrActionCreator === 'string'
640+
? typeOrActionCreator
641+
: typeOrActionCreator.type
642+
if (!type) {
643+
throw new Error(
644+
'`context.addCase` cannot be called with an empty action type'
645+
)
646+
}
647+
if (type in context.sliceCaseReducersByType) {
648+
throw new Error(
649+
'`context.addCase` cannot be called with two reducers for the same action type: ' +
650+
type
651+
)
652+
}
653+
context.sliceCaseReducersByType[type] = reducer
654+
return contextMethods
655+
},
656+
addMatcher(matcher, reducer) {
657+
context.sliceMatchers.push({ matcher, reducer })
658+
return contextMethods
659+
},
660+
exposeAction(name, actionCreator) {
661+
context.actionCreators[name] = actionCreator
662+
return contextMethods
663+
},
664+
exposeCaseReducer(name, reducer) {
665+
context.sliceCaseReducersByName[name] = reducer
666+
return contextMethods
667+
},
668+
}
669+
633670
reducerNames.forEach((reducerName) => {
634671
const reducerDefinition = reducers[reducerName]
635672
const reducerDetails: ReducerDetails = {
@@ -641,14 +678,14 @@ export function buildCreateSlice({ creators }: BuildCreateSliceConfig = {}) {
641678
handleThunkCaseReducerDefinition(
642679
reducerDetails,
643680
reducerDefinition,
644-
context,
681+
contextMethods,
645682
cAT
646683
)
647684
} else {
648685
handleNormalReducerDefinition<State>(
649686
reducerDetails,
650687
reducerDefinition,
651-
context
688+
contextMethods
652689
)
653690
}
654691
})
@@ -803,9 +840,84 @@ interface ReducerHandlingContext<State> {
803840
actionCreators: Record<string, Function>
804841
}
805842

843+
interface ReducerHandlingContextMethods<State> {
844+
/**
845+
* Adds a case reducer to handle a single action type.
846+
* @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.
847+
* @param reducer - The actual case reducer function.
848+
*/
849+
addCase<ActionCreator extends TypedActionCreator<string>>(
850+
actionCreator: ActionCreator,
851+
reducer: CaseReducer<State, ReturnType<ActionCreator>>
852+
): ReducerHandlingContextMethods<State>
853+
/**
854+
* Adds a case reducer to handle a single action type.
855+
* @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.
856+
* @param reducer - The actual case reducer function.
857+
*/
858+
addCase<Type extends string, A extends Action<Type>>(
859+
type: Type,
860+
reducer: CaseReducer<State, A>
861+
): ReducerHandlingContextMethods<State>
862+
863+
/**
864+
* Allows you to match incoming actions against your own filter function instead of only the `action.type` property.
865+
* @remarks
866+
* If multiple matcher reducers match, all of them will be executed in the order
867+
* they were defined in - even if a case reducer already matched.
868+
* All calls to `builder.addMatcher` must come after any calls to `builder.addCase` and before any calls to `builder.addDefaultCase`.
869+
* @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)
870+
* function
871+
* @param reducer - The actual case reducer function.
872+
*
873+
*/
874+
addMatcher<A>(
875+
matcher: TypeGuard<A>,
876+
reducer: CaseReducer<State, A extends Action ? A : A & Action>
877+
): ReducerHandlingContextMethods<State>
878+
/**
879+
* Add an action to be exposed under the final `slice.actions` key.
880+
* @param name The key to be exposed as.
881+
* @param actionCreator The action to expose.
882+
* @example
883+
* context.exposeAction("addPost", createAction<Post>("addPost"));
884+
*
885+
* export const { addPost } = slice.actions
886+
*
887+
* dispatch(addPost(post))
888+
*/
889+
exposeAction(
890+
name: string,
891+
actionCreator: Function
892+
): ReducerHandlingContextMethods<State>
893+
/**
894+
* Add a case reducer to be exposed under the final `slice.caseReducers` key.
895+
* @param name The key to be exposed as.
896+
* @param reducer The reducer to expose.
897+
* @example
898+
* context.exposeCaseReducer("addPost", (state, action: PayloadAction<Post>) => {
899+
* state.push(action.payload)
900+
* })
901+
*
902+
* slice.caseReducers.addPost([], addPost(post))
903+
*/
904+
exposeCaseReducer(
905+
name: string,
906+
reducer:
907+
| CaseReducer<State, any>
908+
| Pick<
909+
AsyncThunkSliceReducerDefinition<State, any, any, any>,
910+
'fulfilled' | 'rejected' | 'pending' | 'settled'
911+
>
912+
): ReducerHandlingContextMethods<State>
913+
}
914+
806915
interface ReducerDetails {
916+
/** The key the reducer was defined under */
807917
reducerName: string
918+
/** The predefined action type, i.e. `${slice.name}/${reducerName}` */
808919
type: string
920+
/** Whether create. notation was used when defining reducers */
809921
createNotation: boolean
810922
}
811923

@@ -852,7 +964,7 @@ function handleNormalReducerDefinition<State>(
852964
maybeReducerWithPrepare:
853965
| CaseReducer<State, { payload: any; type: string }>
854966
| CaseReducerWithPrepare<State, PayloadAction<any, string, any, any>>,
855-
context: ReducerHandlingContext<State>
967+
context: ReducerHandlingContextMethods<State>
856968
) {
857969
let caseReducer: CaseReducer<State, any>
858970
let prepareCallback: PrepareAction<any> | undefined
@@ -870,11 +982,13 @@ function handleNormalReducerDefinition<State>(
870982
} else {
871983
caseReducer = maybeReducerWithPrepare
872984
}
873-
context.sliceCaseReducersByName[reducerName] = caseReducer
874-
context.sliceCaseReducersByType[type] = caseReducer
875-
context.actionCreators[reducerName] = prepareCallback
876-
? createAction(type, prepareCallback)
877-
: createAction(type)
985+
context
986+
.addCase(type, caseReducer)
987+
.exposeCaseReducer(reducerName, caseReducer)
988+
.exposeAction(
989+
reducerName,
990+
prepareCallback ? createAction(type, prepareCallback) : createAction(type)
991+
)
878992
}
879993

880994
function isAsyncThunkSliceReducerDefinition<State>(
@@ -894,7 +1008,7 @@ function isCaseReducerWithPrepareDefinition<State>(
8941008
function handleThunkCaseReducerDefinition<State>(
8951009
{ type, reducerName }: ReducerDetails,
8961010
reducerDefinition: AsyncThunkSliceReducerDefinition<State, any, any, any>,
897-
context: ReducerHandlingContext<State>,
1011+
context: ReducerHandlingContextMethods<State>,
8981012
cAT: typeof _createAsyncThunk | undefined
8991013
) {
9001014
if (!cAT) {
@@ -906,27 +1020,27 @@ function handleThunkCaseReducerDefinition<State>(
9061020
const { payloadCreator, fulfilled, pending, rejected, settled, options } =
9071021
reducerDefinition
9081022
const thunk = cAT(type, payloadCreator, options as any)
909-
context.actionCreators[reducerName] = thunk
1023+
context.exposeAction(reducerName, thunk)
9101024

9111025
if (fulfilled) {
912-
context.sliceCaseReducersByType[thunk.fulfilled.type] = fulfilled
1026+
context.addCase(thunk.fulfilled, fulfilled)
9131027
}
9141028
if (pending) {
915-
context.sliceCaseReducersByType[thunk.pending.type] = pending
1029+
context.addCase(thunk.pending, pending)
9161030
}
9171031
if (rejected) {
918-
context.sliceCaseReducersByType[thunk.rejected.type] = rejected
1032+
context.addCase(thunk.rejected, rejected)
9191033
}
9201034
if (settled) {
921-
context.sliceMatchers.push({ matcher: thunk.settled, reducer: settled })
1035+
context.addMatcher(thunk.settled, settled)
9221036
}
9231037

924-
context.sliceCaseReducersByName[reducerName] = {
1038+
context.exposeCaseReducer(reducerName, {
9251039
fulfilled: fulfilled || noop,
9261040
pending: pending || noop,
9271041
rejected: rejected || noop,
9281042
settled: settled || noop,
929-
}
1043+
})
9301044
}
9311045

9321046
function noop() {}

0 commit comments

Comments
 (0)