Skip to content

Commit cd99bad

Browse files
phryneasmarkerikson
authored andcommitted
allow produce to be swapped out in createReducer/createSlice
1 parent ef49075 commit cd99bad

File tree

3 files changed

+218
-174
lines changed

3 files changed

+218
-174
lines changed

packages/toolkit/src/createReducer.ts

Lines changed: 88 additions & 73 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,90 +147,104 @@ 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>
153-
154-
export function createReducer<S extends NotFunction<any>>(
155-
initialState: S | (() => S),
156-
mapOrBuilderCallback: (builder: ActionReducerMapBuilder<S>) => void
157-
): ReducerWithInitialState<S> {
158-
if (process.env.NODE_ENV !== 'production') {
159-
if (typeof mapOrBuilderCallback === 'object') {
160-
throw new Error(
161-
"The object notation for `createReducer` has been removed. Please use the 'builder callback' notation instead: https://redux-toolkit.js.org/api/createReducer"
162-
)
163-
}
164-
}
150+
<S extends NotFunction<any>>(
151+
initialState: S | (() => S),
152+
builderCallback: (builder: ActionReducerMapBuilder<S>) => void
153+
): ReducerWithInitialState<S>
154+
}
155+
156+
export interface BuildCreateReducerConfiguration {
157+
createNextState: <Base>(
158+
base: Base,
159+
recipe: (draft: Draft<Base>) => void | Base | Draft<Base>
160+
) => Base
161+
}
165162

166-
let [actionsMap, finalActionMatchers, finalDefaultCaseReducer] =
167-
executeReducerBuilderCallback(mapOrBuilderCallback)
163+
export function buildCreateReducer({
164+
createNextState,
165+
}: BuildCreateReducerConfiguration): CreateReducer {
166+
return function createReducer<S extends NotFunction<any>>(
167+
initialState: S | (() => S),
168+
mapOrBuilderCallback: (builder: ActionReducerMapBuilder<S>) => void
169+
): ReducerWithInitialState<S> {
170+
if (process.env.NODE_ENV !== 'production') {
171+
if (typeof mapOrBuilderCallback === 'object') {
172+
throw new Error(
173+
"The object notation for `createReducer` has been removed. Please use the 'builder callback' notation instead: https://redux-toolkit.js.org/api/createReducer"
174+
)
175+
}
176+
}
168177

169-
// Ensure the initial state gets frozen either way (if draftable)
170-
let getInitialState: () => S
171-
if (isStateFunction(initialState)) {
172-
getInitialState = () => freezeDraftable(initialState())
173-
} else {
174-
const frozenInitialState = freezeDraftable(initialState)
175-
getInitialState = () => frozenInitialState
176-
}
178+
let [actionsMap, finalActionMatchers, finalDefaultCaseReducer] =
179+
executeReducerBuilderCallback(mapOrBuilderCallback)
177180

178-
function reducer(state = getInitialState(), action: any): S {
179-
let caseReducers = [
180-
actionsMap[action.type],
181-
...finalActionMatchers
182-
.filter(({ matcher }) => matcher(action))
183-
.map(({ reducer }) => reducer),
184-
]
185-
if (caseReducers.filter((cr) => !!cr).length === 0) {
186-
caseReducers = [finalDefaultCaseReducer]
181+
// Ensure the initial state gets frozen either way (if draftable)
182+
let getInitialState: () => S
183+
if (isStateFunction(initialState)) {
184+
getInitialState = () => freezeDraftable(initialState())
185+
} else {
186+
const frozenInitialState = freezeDraftable(initialState)
187+
getInitialState = () => frozenInitialState
187188
}
188189

189-
return caseReducers.reduce((previousState, caseReducer): S => {
190-
if (caseReducer) {
191-
if (isDraft(previousState)) {
192-
// If it's already a draft, we must already be inside a `createNextState` call,
193-
// likely because this is being wrapped in `createReducer`, `createSlice`, or nested
194-
// inside an existing draft. It's safe to just pass the draft to the mutator.
195-
const draft = previousState as Draft<S> // We can assume this is already a draft
196-
const result = caseReducer(draft, action)
197-
198-
if (result === undefined) {
199-
return previousState
200-
}
190+
function reducer(state = getInitialState(), action: any): S {
191+
let caseReducers = [
192+
actionsMap[action.type],
193+
...finalActionMatchers
194+
.filter(({ matcher }) => matcher(action))
195+
.map(({ reducer }) => reducer),
196+
]
197+
if (caseReducers.filter((cr) => !!cr).length === 0) {
198+
caseReducers = [finalDefaultCaseReducer]
199+
}
201200

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

208-
if (result === undefined) {
209-
if (previousState === null) {
210+
if (result === undefined) {
210211
return previousState
211212
}
212-
throw Error(
213-
'A case reducer on a non-draftable value must not return undefined'
214-
)
215-
}
216213

217-
return result as S
218-
} else {
219-
// @ts-ignore createNextState() produces an Immutable<Draft<S>> rather
220-
// than an Immutable<S>, and TypeScript cannot find out how to reconcile
221-
// these two types.
222-
return createNextState(previousState, (draft: Draft<S>) => {
223-
return caseReducer(draft, action)
224-
})
214+
return result as S
215+
} else if (!isDraftable(previousState)) {
216+
// If state is not draftable (ex: a primitive, such as 0), we want to directly
217+
// return the caseReducer func and not wrap it with produce.
218+
const result = caseReducer(previousState as any, action)
219+
220+
if (result === undefined) {
221+
if (previousState === null) {
222+
return previousState
223+
}
224+
throw Error(
225+
'A case reducer on a non-draftable value must not return undefined'
226+
)
227+
}
228+
229+
return result as S
230+
} else {
231+
// @ts-ignore createNextState() produces an Immutable<Draft<S>> rather
232+
// than an Immutable<S>, and TypeScript cannot find out how to reconcile
233+
// these two types.
234+
return createNextState(previousState, (draft: Draft<S>) => {
235+
return caseReducer(draft, action)
236+
})
237+
}
225238
}
226-
}
227239

228-
return previousState
229-
}, state)
230-
}
240+
return previousState
241+
}, state)
242+
}
231243

232-
reducer.getInitialState = getInitialState
244+
reducer.getInitialState = getInitialState
233245

234-
return reducer as ReducerWithInitialState<S>
246+
return reducer as ReducerWithInitialState<S>
247+
}
235248
}
249+
250+
export const createReducer = buildCreateReducer({ createNextState })

0 commit comments

Comments
 (0)