@@ -13,9 +13,9 @@ import type {
13
13
ReducerWithInitialState ,
14
14
} from './createReducer'
15
15
import { createReducer } from './createReducer'
16
- import type { ActionReducerMapBuilder } from './mapBuilders'
16
+ import type { ActionReducerMapBuilder , TypedActionCreator } from './mapBuilders'
17
17
import { executeReducerBuilderCallback } from './mapBuilders'
18
- import type { Id , Tail } from './tsHelpers'
18
+ import type { Id , Tail , TypeGuard } from './tsHelpers'
19
19
import type { InjectConfig } from './combineSlices'
20
20
import type {
21
21
AsyncThunk ,
@@ -630,6 +630,43 @@ export function buildCreateSlice({ creators }: BuildCreateSliceConfig = {}) {
630
630
sliceMatchers : [ ] ,
631
631
}
632
632
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
+
633
670
reducerNames . forEach ( ( reducerName ) => {
634
671
const reducerDefinition = reducers [ reducerName ]
635
672
const reducerDetails : ReducerDetails = {
@@ -641,14 +678,14 @@ export function buildCreateSlice({ creators }: BuildCreateSliceConfig = {}) {
641
678
handleThunkCaseReducerDefinition (
642
679
reducerDetails ,
643
680
reducerDefinition ,
644
- context ,
681
+ contextMethods ,
645
682
cAT
646
683
)
647
684
} else {
648
685
handleNormalReducerDefinition < State > (
649
686
reducerDetails ,
650
687
reducerDefinition ,
651
- context
688
+ contextMethods
652
689
)
653
690
}
654
691
} )
@@ -803,9 +840,84 @@ interface ReducerHandlingContext<State> {
803
840
actionCreators : Record < string , Function >
804
841
}
805
842
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
+
806
915
interface ReducerDetails {
916
+ /** The key the reducer was defined under */
807
917
reducerName : string
918
+ /** The predefined action type, i.e. `${slice.name}/${reducerName}` */
808
919
type : string
920
+ /** Whether create. notation was used when defining reducers */
809
921
createNotation : boolean
810
922
}
811
923
@@ -852,7 +964,7 @@ function handleNormalReducerDefinition<State>(
852
964
maybeReducerWithPrepare :
853
965
| CaseReducer < State , { payload : any ; type : string } >
854
966
| CaseReducerWithPrepare < State , PayloadAction < any , string , any , any > > ,
855
- context : ReducerHandlingContext < State >
967
+ context : ReducerHandlingContextMethods < State >
856
968
) {
857
969
let caseReducer : CaseReducer < State , any >
858
970
let prepareCallback : PrepareAction < any > | undefined
@@ -870,11 +982,13 @@ function handleNormalReducerDefinition<State>(
870
982
} else {
871
983
caseReducer = maybeReducerWithPrepare
872
984
}
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
+ )
878
992
}
879
993
880
994
function isAsyncThunkSliceReducerDefinition < State > (
@@ -894,7 +1008,7 @@ function isCaseReducerWithPrepareDefinition<State>(
894
1008
function handleThunkCaseReducerDefinition < State > (
895
1009
{ type, reducerName } : ReducerDetails ,
896
1010
reducerDefinition : AsyncThunkSliceReducerDefinition < State , any , any , any > ,
897
- context : ReducerHandlingContext < State > ,
1011
+ context : ReducerHandlingContextMethods < State > ,
898
1012
cAT : typeof _createAsyncThunk | undefined
899
1013
) {
900
1014
if ( ! cAT ) {
@@ -906,27 +1020,27 @@ function handleThunkCaseReducerDefinition<State>(
906
1020
const { payloadCreator, fulfilled, pending, rejected, settled, options } =
907
1021
reducerDefinition
908
1022
const thunk = cAT ( type , payloadCreator , options as any )
909
- context . actionCreators [ reducerName ] = thunk
1023
+ context . exposeAction ( reducerName , thunk )
910
1024
911
1025
if ( fulfilled ) {
912
- context . sliceCaseReducersByType [ thunk . fulfilled . type ] = fulfilled
1026
+ context . addCase ( thunk . fulfilled , fulfilled )
913
1027
}
914
1028
if ( pending ) {
915
- context . sliceCaseReducersByType [ thunk . pending . type ] = pending
1029
+ context . addCase ( thunk . pending , pending )
916
1030
}
917
1031
if ( rejected ) {
918
- context . sliceCaseReducersByType [ thunk . rejected . type ] = rejected
1032
+ context . addCase ( thunk . rejected , rejected )
919
1033
}
920
1034
if ( settled ) {
921
- context . sliceMatchers . push ( { matcher : thunk . settled , reducer : settled } )
1035
+ context . addMatcher ( thunk . settled , settled )
922
1036
}
923
1037
924
- context . sliceCaseReducersByName [ reducerName ] = {
1038
+ context . exposeCaseReducer ( reducerName , {
925
1039
fulfilled : fulfilled || noop ,
926
1040
pending : pending || noop ,
927
1041
rejected : rejected || noop ,
928
1042
settled : settled || noop ,
929
- }
1043
+ } )
930
1044
}
931
1045
932
1046
function noop ( ) { }
0 commit comments