Skip to content

Commit 0ce2ec0

Browse files
committed
allow produce to be swapped out in createReducer/createSlice
1 parent 496fe5c commit 0ce2ec0

File tree

3 files changed

+238
-194
lines changed

3 files changed

+238
-194
lines changed

packages/toolkit/src/createReducer.ts

Lines changed: 106 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ export type ReducerWithInitialState<S extends NotFunction<any>> = Reducer<S> & {
8282

8383
let hasWarnedAboutObjectNotation = false
8484

85-
/**
85+
export type CreateReducer = {
86+
/**
8687
* A utility function that allows defining a reducer as a mapping from action
8788
* type to *case reducer* functions that handle these action types. The
8889
* reducer's initial state is passed as the first argument.
@@ -146,12 +147,12 @@ const reducer = createReducer(
146147
```
147148
* @public
148149
*/
149-
export function createReducer<S extends NotFunction<any>>(
150-
initialState: S | (() => S),
151-
builderCallback: (builder: ActionReducerMapBuilder<S>) => void
152-
): ReducerWithInitialState<S>
150+
<S extends NotFunction<any>>(
151+
initialState: S | (() => S),
152+
builderCallback: (builder: ActionReducerMapBuilder<S>) => void
153+
): ReducerWithInitialState<S>
153154

154-
/**
155+
/**
155156
* A utility function that allows defining a reducer as a mapping from action
156157
* type to *case reducer* functions that handle these action types. The
157158
* reducer's initial state is passed as the first argument.
@@ -203,104 +204,118 @@ const counterReducer = createReducer(0, {
203204
```
204205
* @public
205206
*/
206-
export function createReducer<
207-
S extends NotFunction<any>,
208-
CR extends CaseReducers<S, any> = CaseReducers<S, any>
209-
>(
210-
initialState: S | (() => S),
211-
actionsMap: CR,
212-
actionMatchers?: ActionMatcherDescriptionCollection<S>,
213-
defaultCaseReducer?: CaseReducer<S>
214-
): ReducerWithInitialState<S>
215-
216-
export function createReducer<S extends NotFunction<any>>(
217-
initialState: S | (() => S),
218-
mapOrBuilderCallback:
219-
| CaseReducers<S, any>
220-
| ((builder: ActionReducerMapBuilder<S>) => void),
221-
actionMatchers: ReadonlyActionMatcherDescriptionCollection<S> = [],
222-
defaultCaseReducer?: CaseReducer<S>
223-
): ReducerWithInitialState<S> {
224-
if (process.env.NODE_ENV !== 'production') {
225-
if (typeof mapOrBuilderCallback === 'object') {
226-
if (!hasWarnedAboutObjectNotation) {
227-
hasWarnedAboutObjectNotation = true
228-
console.warn(
229-
"The object notation for `createReducer` is deprecated, and will be removed in RTK 2.0. Please use the 'builder callback' notation instead: https://redux-toolkit.js.org/api/createReducer"
230-
)
207+
<
208+
S extends NotFunction<any>,
209+
CR extends CaseReducers<S, any> = CaseReducers<S, any>
210+
>(
211+
initialState: S | (() => S),
212+
actionsMap: CR,
213+
actionMatchers?: ActionMatcherDescriptionCollection<S>,
214+
defaultCaseReducer?: CaseReducer<S>
215+
): ReducerWithInitialState<S>
216+
}
217+
218+
export interface BuildCreateReducerConfiguration {
219+
createNextState: <Base>(
220+
base: Base,
221+
recipe: (draft: Draft<Base>) => void | Base | Draft<Base>
222+
) => Base
223+
}
224+
225+
export function buildCreateReducer({
226+
createNextState,
227+
}: BuildCreateReducerConfiguration): CreateReducer {
228+
return function createReducer<S extends NotFunction<any>>(
229+
initialState: S | (() => S),
230+
mapOrBuilderCallback:
231+
| CaseReducers<S, any>
232+
| ((builder: ActionReducerMapBuilder<S>) => void),
233+
actionMatchers: ReadonlyActionMatcherDescriptionCollection<S> = [],
234+
defaultCaseReducer?: CaseReducer<S>
235+
): ReducerWithInitialState<S> {
236+
if (process.env.NODE_ENV !== 'production') {
237+
if (typeof mapOrBuilderCallback === 'object') {
238+
if (!hasWarnedAboutObjectNotation) {
239+
hasWarnedAboutObjectNotation = true
240+
console.warn(
241+
"The object notation for `createReducer` is deprecated, and will be removed in RTK 2.0. Please use the 'builder callback' notation instead: https://redux-toolkit.js.org/api/createReducer"
242+
)
243+
}
231244
}
232245
}
233-
}
234246

235-
let [actionsMap, finalActionMatchers, finalDefaultCaseReducer] =
236-
typeof mapOrBuilderCallback === 'function'
237-
? executeReducerBuilderCallback(mapOrBuilderCallback)
238-
: [mapOrBuilderCallback, actionMatchers, defaultCaseReducer]
239-
240-
// Ensure the initial state gets frozen either way (if draftable)
241-
let getInitialState: () => S
242-
if (isStateFunction(initialState)) {
243-
getInitialState = () => freezeDraftable(initialState())
244-
} else {
245-
const frozenInitialState = freezeDraftable(initialState)
246-
getInitialState = () => frozenInitialState
247-
}
247+
let [actionsMap, finalActionMatchers, finalDefaultCaseReducer] =
248+
typeof mapOrBuilderCallback === 'function'
249+
? executeReducerBuilderCallback(mapOrBuilderCallback)
250+
: [mapOrBuilderCallback, actionMatchers, defaultCaseReducer]
248251

249-
function reducer(state = getInitialState(), action: any): S {
250-
let caseReducers = [
251-
actionsMap[action.type],
252-
...finalActionMatchers
253-
.filter(({ matcher }) => matcher(action))
254-
.map(({ reducer }) => reducer),
255-
]
256-
if (caseReducers.filter((cr) => !!cr).length === 0) {
257-
caseReducers = [finalDefaultCaseReducer]
252+
// Ensure the initial state gets frozen either way (if draftable)
253+
let getInitialState: () => S
254+
if (isStateFunction(initialState)) {
255+
getInitialState = () => freezeDraftable(initialState())
256+
} else {
257+
const frozenInitialState = freezeDraftable(initialState)
258+
getInitialState = () => frozenInitialState
258259
}
259260

260-
return caseReducers.reduce((previousState, caseReducer): S => {
261-
if (caseReducer) {
262-
if (isDraft(previousState)) {
263-
// If it's already a draft, we must already be inside a `createNextState` call,
264-
// likely because this is being wrapped in `createReducer`, `createSlice`, or nested
265-
// inside an existing draft. It's safe to just pass the draft to the mutator.
266-
const draft = previousState as Draft<S> // We can assume this is already a draft
267-
const result = caseReducer(draft, action)
268-
269-
if (result === undefined) {
270-
return previousState
271-
}
261+
function reducer(state = getInitialState(), action: any): S {
262+
let caseReducers = [
263+
actionsMap[action.type],
264+
...finalActionMatchers
265+
.filter(({ matcher }) => matcher(action))
266+
.map(({ reducer }) => reducer),
267+
]
268+
if (caseReducers.filter((cr) => !!cr).length === 0) {
269+
caseReducers = [finalDefaultCaseReducer]
270+
}
272271

273-
return result as S
274-
} else if (!isDraftable(previousState)) {
275-
// If state is not draftable (ex: a primitive, such as 0), we want to directly
276-
// return the caseReducer func and not wrap it with produce.
277-
const result = caseReducer(previousState as any, action)
272+
return caseReducers.reduce((previousState, caseReducer): S => {
273+
if (caseReducer) {
274+
if (isDraft(previousState)) {
275+
// If it's already a draft, we must already be inside a `createNextState` call,
276+
// likely because this is being wrapped in `createReducer`, `createSlice`, or nested
277+
// inside an existing draft. It's safe to just pass the draft to the mutator.
278+
const draft = previousState as Draft<S> // We can assume this is already a draft
279+
const result = caseReducer(draft, action)
278280

279-
if (result === undefined) {
280-
if (previousState === null) {
281+
if (result === undefined) {
281282
return previousState
282283
}
283-
throw Error(
284-
'A case reducer on a non-draftable value must not return undefined'
285-
)
286-
}
287284

288-
return result as S
289-
} else {
290-
// @ts-ignore createNextState() produces an Immutable<Draft<S>> rather
291-
// than an Immutable<S>, and TypeScript cannot find out how to reconcile
292-
// these two types.
293-
return createNextState(previousState, (draft: Draft<S>) => {
294-
return caseReducer(draft, action)
295-
})
285+
return result as S
286+
} else if (!isDraftable(previousState)) {
287+
// If state is not draftable (ex: a primitive, such as 0), we want to directly
288+
// return the caseReducer func and not wrap it with produce.
289+
const result = caseReducer(previousState as any, action)
290+
291+
if (result === undefined) {
292+
if (previousState === null) {
293+
return previousState
294+
}
295+
throw Error(
296+
'A case reducer on a non-draftable value must not return undefined'
297+
)
298+
}
299+
300+
return result as S
301+
} else {
302+
// @ts-ignore createNextState() produces an Immutable<Draft<S>> rather
303+
// than an Immutable<S>, and TypeScript cannot find out how to reconcile
304+
// these two types.
305+
return createNextState(previousState, (draft: Draft<S>) => {
306+
return caseReducer(draft, action)
307+
})
308+
}
296309
}
297-
}
298310

299-
return previousState
300-
}, state)
301-
}
311+
return previousState
312+
}, state)
313+
}
302314

303-
reducer.getInitialState = getInitialState
315+
reducer.getInitialState = getInitialState
304316

305-
return reducer as ReducerWithInitialState<S>
317+
return reducer as ReducerWithInitialState<S>
318+
}
306319
}
320+
321+
export const createReducer = buildCreateReducer({ createNextState })

0 commit comments

Comments
 (0)