Skip to content

Commit a4ba0ce

Browse files
committed
Experiment with allowing providing custom slice creators when calling createSlice
1 parent 8d61a90 commit a4ba0ce

File tree

3 files changed

+96
-18
lines changed

3 files changed

+96
-18
lines changed

errors.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,6 @@
4646
"44": "called \\`injectEndpoints\\` to override already-existing endpointName without specifying \\`overrideExisting: true\\`",
4747
"45": "context.exposeAction cannot be called twice for the same reducer definition: reducerName",
4848
"46": "context.exposeCaseReducer cannot be called twice for the same reducer definition: reducerName",
49-
"47": "Could not find \"\" slice in state. In order for slice creators to use \\`context.selectSlice\\`, the slice must be nested in the state under its reducerPath: \"\""
50-
}
49+
"47": "Could not find \"\" slice in state. In order for slice creators to use \\`context.selectSlice\\`, the slice must be nested in the state under its reducerPath: \"\"",
50+
"48": "A creator with the name has already been provided to buildCreateSlice"
51+
}

packages/toolkit/src/createSlice.ts

Lines changed: 72 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,14 @@ interface InternalReducerHandlingContext<State> {
208208

209209
sliceCaseReducersByName: Record<string, any>
210210
actionCreators: Record<string, any>
211+
212+
sliceCreators: Record<string, ReducerCreator<RegisteredReducerType>['create']>
213+
sliceCreatorHandlers: Partial<
214+
Record<
215+
RegisteredReducerType,
216+
ReducerCreator<RegisteredReducerType>['handle']
217+
>
218+
>
211219
}
212220

213221
export interface ReducerHandlingContext<State> {
@@ -530,12 +538,13 @@ export interface CreateSliceOptions<
530538
State,
531539
Name,
532540
ReducerPath,
533-
CreatorMap
541+
CreatorMap & SliceCreatorMap
534542
> = SliceCaseReducers<State>,
535543
Name extends string = string,
536544
ReducerPath extends string = Name,
537545
Selectors extends SliceSelectors<State> = SliceSelectors<State>,
538546
CreatorMap extends Record<string, RegisteredReducerType> = {},
547+
SliceCreatorMap extends Record<string, RegisteredReducerType> = {},
539548
> {
540549
/**
541550
* The slice's name. Used to namespace the generated action types.
@@ -607,6 +616,10 @@ createSlice({
607616
* A map of selectors that receive the slice's state and any additional arguments, and return a result.
608617
*/
609618
selectors?: Selectors
619+
620+
creators?: CreatorOption<SliceCreatorMap> & {
621+
[K in keyof CreatorMap]?: never
622+
}
610623
}
611624

612625
export interface CaseReducerDefinition<
@@ -837,14 +850,18 @@ const isCreatorCallback = (
837850
): reducers is CreatorCallback<any, any, any, any> =>
838851
typeof reducers === 'function'
839852

853+
type CreatorOption<CreatorMap extends Record<string, RegisteredReducerType>> = {
854+
[Name in keyof CreatorMap]: Name extends 'reducer' | 'preparedReducer'
855+
? never
856+
: ReducerCreator<CreatorMap[Name]>
857+
} & {
858+
asyncThunk?: ReducerCreator<ReducerType.asyncThunk>
859+
}
860+
840861
interface BuildCreateSliceConfig<
841862
CreatorMap extends Record<string, RegisteredReducerType>,
842863
> {
843-
creators?: {
844-
[Name in keyof CreatorMap]: Name extends 'reducer' | 'preparedReducer'
845-
? never
846-
: ReducerCreator<CreatorMap[Name]>
847-
} & { asyncThunk?: ReducerCreator<ReducerType.asyncThunk> }
864+
creators?: CreatorOption<CreatorMap>
848865
}
849866

850867
export function buildCreateSlice<
@@ -901,27 +918,42 @@ export function buildCreateSlice<
901918
State,
902919
CaseReducers extends
903920
| SliceCaseReducers<State>
904-
| CreatorCallback<State, Name, ReducerPath, CreatorMap>,
921+
| CreatorCallback<State, Name, ReducerPath, CreatorMap & SliceCreatorMap>,
905922
Name extends string,
906923
Selectors extends SliceSelectors<State>,
907924
ReducerPath extends string = Name,
925+
SliceCreatorMap extends Record<string, RegisteredReducerType> = {},
908926
>(
909927
options: CreateSliceOptions<
910928
State,
911929
CaseReducers,
912930
Name,
913931
ReducerPath,
914932
Selectors,
915-
CreatorMap
933+
CreatorMap,
934+
SliceCreatorMap
916935
>,
917936
): Slice<
918937
State,
919-
GetCaseReducers<State, Name, ReducerPath, CreatorMap, CaseReducers>,
938+
GetCaseReducers<
939+
State,
940+
Name,
941+
ReducerPath,
942+
CreatorMap & SliceCreatorMap,
943+
CaseReducers
944+
>,
920945
Name,
921946
ReducerPath,
922947
Selectors
923948
> {
924-
const { name, reducerPath = name as unknown as ReducerPath } = options
949+
const {
950+
name,
951+
reducerPath = name as unknown as ReducerPath,
952+
creators: sliceCreators = {} as Record<
953+
string,
954+
ReducerCreator<RegisteredReducerType>
955+
>,
956+
} = options
925957
if (!name) {
926958
throw new Error('`name` is a required option for createSlice')
927959
}
@@ -944,6 +976,20 @@ export function buildCreateSlice<
944976
sliceCaseReducersByType: {},
945977
actionCreators: {},
946978
sliceMatchers: [],
979+
sliceCreators: { ...creators },
980+
sliceCreatorHandlers: { ...handlers },
981+
}
982+
983+
for (const [name, creator] of Object.entries(sliceCreators)) {
984+
if (name in creators) {
985+
throw new Error(
986+
`A creator with the name ${name} has already been provided to buildCreateSlice`,
987+
)
988+
}
989+
internalContext.sliceCreators[name] = creator.create
990+
if ('handle' in creator) {
991+
internalContext.sliceCreatorHandlers[creator.type] = creator.handle
992+
}
947993
}
948994

949995
function getContext({ reducerName }: ReducerDetails) {
@@ -1009,15 +1055,15 @@ export function buildCreateSlice<
10091055
}
10101056

10111057
if (isCreatorCallback(options.reducers)) {
1012-
const reducers = options.reducers(creators as any)
1058+
const reducers = options.reducers(internalContext.sliceCreators as any)
10131059
for (const [reducerName, reducerDefinition] of Object.entries(reducers)) {
10141060
const { _reducerDefinitionType: type } = reducerDefinition
10151061
if (typeof type === 'undefined') {
10161062
throw new Error(
10171063
'Please use reducer creators passed to callback. Each reducer definition must have a `_reducerDefinitionType` property indicating which handler to use.',
10181064
)
10191065
}
1020-
const handle = handlers[type]
1066+
const handle = internalContext.sliceCreatorHandlers[type]
10211067
if (!handle) {
10221068
throw new Error(`Unsupported reducer type: ${String(type)}`)
10231069
}
@@ -1117,7 +1163,13 @@ export function buildCreateSlice<
11171163
): Pick<
11181164
Slice<
11191165
State,
1120-
GetCaseReducers<State, Name, ReducerPath, CreatorMap, CaseReducers>,
1166+
GetCaseReducers<
1167+
State,
1168+
Name,
1169+
ReducerPath,
1170+
CreatorMap & SliceCreatorMap,
1171+
CaseReducers
1172+
>,
11211173
Name,
11221174
CurrentReducerPath,
11231175
Selectors
@@ -1173,7 +1225,13 @@ export function buildCreateSlice<
11731225

11741226
const slice: Slice<
11751227
State,
1176-
GetCaseReducers<State, Name, ReducerPath, CreatorMap, CaseReducers>,
1228+
GetCaseReducers<
1229+
State,
1230+
Name,
1231+
ReducerPath,
1232+
CreatorMap & SliceCreatorMap,
1233+
CaseReducers
1234+
>,
11771235
Name,
11781236
ReducerPath,
11791237
Selectors

packages/toolkit/src/tests/createSlice.test.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import type {
1212
ReducerCreatorEntry,
1313
ReducerCreators,
1414
ReducerDefinition,
15-
ReducerDetails,
1615
ReducerHandlingContext,
1716
SliceActionType,
1817
ThunkAction,
@@ -36,6 +35,7 @@ import {
3635
mockConsole,
3736
} from 'console-testing-library/pure'
3837
import type { IfMaybeUndefined, NoInfer } from '../tsHelpers'
38+
import { delay } from 'msw'
3939
enablePatches()
4040

4141
type CreateSlice = typeof createSlice
@@ -746,7 +746,9 @@ describe('createSlice', () => {
746746
addLoader: loaderCreator.create({}),
747747
}),
748748
}),
749-
).toThrowErrorMatchingInlineSnapshot(`[Error: Unsupported reducer type: Symbol(loaderCreatorType)]`)
749+
).toThrowErrorMatchingInlineSnapshot(
750+
`[Error: Unsupported reducer type: Symbol(loaderCreatorType)]`,
751+
)
750752
const createAppSlice = buildCreateSlice({
751753
creators: { loader: loaderCreator },
752754
})
@@ -1101,6 +1103,23 @@ describe('createSlice', () => {
11011103
)
11021104
})
11031105
})
1106+
test('creators can be provided per createSlice call', () => {
1107+
const loaderSlice = createSlice({
1108+
name: 'loader',
1109+
initialState: {} as Partial<Record<string, true>>,
1110+
creators: { loader: loaderCreator },
1111+
reducers: (create) => ({
1112+
addLoader: create.loader({}),
1113+
}),
1114+
})
1115+
expect(loaderSlice.actions.addLoader).toEqual(expect.any(Function))
1116+
expect(loaderSlice.actions.addLoader.started).toEqual(
1117+
expect.any(Function),
1118+
)
1119+
expect(loaderSlice.actions.addLoader.started.type).toBe(
1120+
'loader/addLoader/started',
1121+
)
1122+
})
11041123
})
11051124
})
11061125

0 commit comments

Comments
 (0)