Skip to content

Commit

Permalink
allow produce to be swapped out in createReducer/createSlice
Browse files Browse the repository at this point in the history
  • Loading branch information
phryneas authored and markerikson committed Feb 21, 2023
1 parent ef49075 commit cd99bad
Show file tree
Hide file tree
Showing 3 changed files with 218 additions and 174 deletions.
161 changes: 88 additions & 73 deletions packages/toolkit/src/createReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ export type ReducerWithInitialState<S extends NotFunction<any>> = Reducer<S> & {

let hasWarnedAboutObjectNotation = false

/**
export type CreateReducer = {
/**
* A utility function that allows defining a reducer as a mapping from action
* type to *case reducer* functions that handle these action types. The
* reducer's initial state is passed as the first argument.
Expand Down Expand Up @@ -146,90 +147,104 @@ const reducer = createReducer(
```
* @public
*/
export function createReducer<S extends NotFunction<any>>(
initialState: S | (() => S),
builderCallback: (builder: ActionReducerMapBuilder<S>) => void
): ReducerWithInitialState<S>

export function createReducer<S extends NotFunction<any>>(
initialState: S | (() => S),
mapOrBuilderCallback: (builder: ActionReducerMapBuilder<S>) => void
): ReducerWithInitialState<S> {
if (process.env.NODE_ENV !== 'production') {
if (typeof mapOrBuilderCallback === 'object') {
throw new Error(
"The object notation for `createReducer` has been removed. Please use the 'builder callback' notation instead: https://redux-toolkit.js.org/api/createReducer"
)
}
}
<S extends NotFunction<any>>(
initialState: S | (() => S),
builderCallback: (builder: ActionReducerMapBuilder<S>) => void
): ReducerWithInitialState<S>
}

export interface BuildCreateReducerConfiguration {
createNextState: <Base>(
base: Base,
recipe: (draft: Draft<Base>) => void | Base | Draft<Base>
) => Base
}

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

// Ensure the initial state gets frozen either way (if draftable)
let getInitialState: () => S
if (isStateFunction(initialState)) {
getInitialState = () => freezeDraftable(initialState())
} else {
const frozenInitialState = freezeDraftable(initialState)
getInitialState = () => frozenInitialState
}
let [actionsMap, finalActionMatchers, finalDefaultCaseReducer] =
executeReducerBuilderCallback(mapOrBuilderCallback)

function reducer(state = getInitialState(), action: any): S {
let caseReducers = [
actionsMap[action.type],
...finalActionMatchers
.filter(({ matcher }) => matcher(action))
.map(({ reducer }) => reducer),
]
if (caseReducers.filter((cr) => !!cr).length === 0) {
caseReducers = [finalDefaultCaseReducer]
// Ensure the initial state gets frozen either way (if draftable)
let getInitialState: () => S
if (isStateFunction(initialState)) {
getInitialState = () => freezeDraftable(initialState())
} else {
const frozenInitialState = freezeDraftable(initialState)
getInitialState = () => frozenInitialState
}

return caseReducers.reduce((previousState, caseReducer): S => {
if (caseReducer) {
if (isDraft(previousState)) {
// If it's already a draft, we must already be inside a `createNextState` call,
// likely because this is being wrapped in `createReducer`, `createSlice`, or nested
// inside an existing draft. It's safe to just pass the draft to the mutator.
const draft = previousState as Draft<S> // We can assume this is already a draft
const result = caseReducer(draft, action)

if (result === undefined) {
return previousState
}
function reducer(state = getInitialState(), action: any): S {
let caseReducers = [
actionsMap[action.type],
...finalActionMatchers
.filter(({ matcher }) => matcher(action))
.map(({ reducer }) => reducer),
]
if (caseReducers.filter((cr) => !!cr).length === 0) {
caseReducers = [finalDefaultCaseReducer]
}

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

if (result === undefined) {
if (previousState === null) {
if (result === undefined) {
return previousState
}
throw Error(
'A case reducer on a non-draftable value must not return undefined'
)
}

return result as S
} else {
// @ts-ignore createNextState() produces an Immutable<Draft<S>> rather
// than an Immutable<S>, and TypeScript cannot find out how to reconcile
// these two types.
return createNextState(previousState, (draft: Draft<S>) => {
return caseReducer(draft, action)
})
return result as S
} else if (!isDraftable(previousState)) {
// If state is not draftable (ex: a primitive, such as 0), we want to directly
// return the caseReducer func and not wrap it with produce.
const result = caseReducer(previousState as any, action)

if (result === undefined) {
if (previousState === null) {
return previousState
}
throw Error(
'A case reducer on a non-draftable value must not return undefined'
)
}

return result as S
} else {
// @ts-ignore createNextState() produces an Immutable<Draft<S>> rather
// than an Immutable<S>, and TypeScript cannot find out how to reconcile
// these two types.
return createNextState(previousState, (draft: Draft<S>) => {
return caseReducer(draft, action)
})
}
}
}

return previousState
}, state)
}
return previousState
}, state)
}

reducer.getInitialState = getInitialState
reducer.getInitialState = getInitialState

return reducer as ReducerWithInitialState<S>
return reducer as ReducerWithInitialState<S>
}
}

export const createReducer = buildCreateReducer({ createNextState })
Loading

0 comments on commit cd99bad

Please sign in to comment.