Skip to content

Commit e18e23c

Browse files
author
Lenz Weber
committed
add create.preparedReducer
1 parent 6b65972 commit e18e23c

File tree

3 files changed

+127
-4
lines changed

3 files changed

+127
-4
lines changed

src/createAction.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,26 @@ export type PrepareAction<P> =
5050
| ((...args: any[]) => { payload: P; error: any })
5151
| ((...args: any[]) => { payload: P; meta: any; error: any })
5252

53+
export type _PayloadActionForPrepare<
54+
PA extends PrepareAction<any>,
55+
T extends string = string
56+
> = PA extends PrepareAction<infer P>
57+
? PayloadAction<
58+
P,
59+
T,
60+
ReturnType<PA> extends {
61+
meta: infer M
62+
}
63+
? M
64+
: never,
65+
ReturnType<PA> extends {
66+
error: infer E
67+
}
68+
? E
69+
: never
70+
>
71+
: never
72+
5373
/**
5474
* Internal version of `ActionCreatorWithPreparedPayload`. Not to be used externally.
5575
*

src/createSlice.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,5 +379,62 @@ describe('createSlice', () => {
379379
)
380380
).not.toThrow()
381381
})
382+
383+
test('can define reducer with prepare statement using create.preparedReducer', async () => {
384+
const slice = createSlice({
385+
name: 'test',
386+
initialState: [] as any[],
387+
reducers: create => ({
388+
prepared: create.preparedReducer(
389+
(p: string, m: number, e: { message: string }) => ({
390+
payload: p,
391+
meta: m,
392+
error: e
393+
}),
394+
(state, action) => {
395+
state.push(action)
396+
}
397+
)
398+
})
399+
})
400+
401+
expect(
402+
slice.reducer([], slice.actions.prepared('test', 1, { message: 'err' }))
403+
).toMatchInlineSnapshot(`
404+
Array [
405+
Object {
406+
"error": Object {
407+
"message": "err",
408+
},
409+
"meta": 1,
410+
"payload": "test",
411+
"type": "test/prepared",
412+
},
413+
]
414+
`)
415+
})
416+
417+
test('throws an error when invoked with a normal `prepare` object that has not gone through a `create.preparedReducer` call', async () => {
418+
expect(() =>
419+
createSlice({
420+
name: 'test',
421+
initialState: [] as any[],
422+
reducers: create => ({
423+
prepared: {
424+
prepare: (p: string, m: number, e: { message: string }) => ({
425+
payload: p,
426+
meta: m,
427+
error: e
428+
}),
429+
reducer: (state, action) => {
430+
state.push(action)
431+
}
432+
}
433+
})
434+
})
435+
).toThrowErrorMatchingInlineSnapshot(
436+
`"Please use the \`create.preparedReducer\` notation for prepared action creators with the \`create\` notation."`
437+
)
438+
})
382439
})
383440
})

src/createSlice.ts

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
PayloadAction,
66
PayloadActionCreator,
77
PrepareAction,
8-
_ActionCreatorWithPreparedPayload
8+
_ActionCreatorWithPreparedPayload,
9+
_PayloadActionForPrepare
910
} from './createAction'
1011
import { CaseReducer, CaseReducers, createReducer } from './createReducer'
1112
import {
@@ -109,7 +110,8 @@ export interface CreateSliceOptions<
109110

110111
const reducerDefinitionType: unique symbol = Symbol('reducerType')
111112
const enum ReducerType {
112-
asyncThunk = 0
113+
asyncThunk = 1,
114+
reducerWithPrepare = 2
113115
}
114116

115117
/**
@@ -159,6 +161,13 @@ export interface AsyncThunkSliceReducerDefinition<
159161
payloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg, ThunkApiConfig>
160162
}
161163

164+
export interface CaseReducerWithPrepareDefinition<
165+
State,
166+
Action extends PayloadAction
167+
> extends CaseReducerWithPrepare<State, Action> {
168+
[reducerDefinitionType]: ReducerType.reducerWithPrepare
169+
}
170+
162171
interface ReducerCreators<State> {
163172
asyncThunk<
164173
ThunkArg extends any,
@@ -177,6 +186,15 @@ interface ReducerCreators<State> {
177186
ThunkApiConfig
178187
>
179188
): AsyncThunkSliceReducerDefinition<State, ThunkArg, Returned, ThunkApiConfig>
189+
190+
preparedReducer<Prepare extends PrepareAction<any>>(
191+
prepare: Prepare,
192+
reducer: CaseReducer<State, _PayloadActionForPrepare<Prepare>>
193+
): {
194+
[reducerDefinitionType]: ReducerType.reducerWithPrepare
195+
prepare: Prepare
196+
reducer: CaseReducer<State, _PayloadActionForPrepare<Prepare>>
197+
}
180198
}
181199

182200
/**
@@ -325,7 +343,11 @@ export function createSlice<
325343

326344
reducerNames.forEach(reducerName => {
327345
const reducerDefinition = reducers[reducerName]
328-
const reducerDetails = { reducerName, type: getType(name, reducerName) }
346+
const reducerDetails = {
347+
reducerName,
348+
type: getType(name, reducerName),
349+
isCreateNotation: typeof options.reducers === 'function'
350+
}
329351

330352
if (isAsyncThunkSliceReducerDefinition<State>(reducerDefinition)) {
331353
handleThunkCaseReducerDefinition(
@@ -379,6 +401,7 @@ interface ReducerHandlingContext<State> {
379401
interface ReducerDetails {
380402
reducerName: string
381403
type: string
404+
createNotation: boolean
382405
}
383406

384407
function getReducerCreators<State>(): ReducerCreators<State> {
@@ -389,12 +412,19 @@ function getReducerCreators<State>(): ReducerCreators<State> {
389412
...config,
390413
payloadCreator
391414
}
415+
},
416+
preparedReducer(prepare, reducer) {
417+
return {
418+
[reducerDefinitionType]: ReducerType.reducerWithPrepare,
419+
prepare,
420+
reducer
421+
}
392422
}
393423
}
394424
}
395425

396426
function handleNormalReducerDefinition<State>(
397-
{ type, reducerName }: ReducerDetails,
427+
{ type, reducerName, createNotation }: ReducerDetails,
398428
maybeReducerWithPrepare:
399429
| CaseReducer<State, { payload: any; type: string }>
400430
| CaseReducerWithPrepare<State, PayloadAction<any, string, any, any>>,
@@ -403,6 +433,14 @@ function handleNormalReducerDefinition<State>(
403433
let caseReducer: CaseReducer<State, any>
404434
let prepareCallback: PrepareAction<any> | undefined
405435
if ('reducer' in maybeReducerWithPrepare) {
436+
if (
437+
createNotation &&
438+
!isCaseReducerWithPrepareDefinition(maybeReducerWithPrepare)
439+
) {
440+
throw new Error(
441+
'Please use the `create.preparedReducer` notation for prepared action creators with the `create` notation.'
442+
)
443+
}
406444
caseReducer = maybeReducerWithPrepare.reducer
407445
prepareCallback = maybeReducerWithPrepare.prepare
408446
} else {
@@ -421,6 +459,14 @@ function isAsyncThunkSliceReducerDefinition<State>(
421459
return reducerDefinition[reducerDefinitionType] === ReducerType.asyncThunk
422460
}
423461

462+
function isCaseReducerWithPrepareDefinition<State>(
463+
reducerDefinition: any
464+
): reducerDefinition is CaseReducerWithPrepareDefinition<State, any> {
465+
return (
466+
reducerDefinition[reducerDefinitionType] === ReducerType.reducerWithPrepare
467+
)
468+
}
469+
424470
function handleThunkCaseReducerDefinition<State>(
425471
{ type, reducerName }: ReducerDetails,
426472
reducerDefinition: AsyncThunkSliceReducerDefinition<State, any, any, any>,

0 commit comments

Comments
 (0)