Skip to content

Commit 9a00f7b

Browse files
AndrewCraswellAndrew CraswellJomik
authored
Allow nested Immer produce statements with createReducer (#509)
* Allow nested produce statements without swallowing in the innermost produce's changes * Resolve linting errors * Update src/createReducer.ts Co-Authored-By: Jonas Holst Damtoft <Jomik@users.noreply.github.com> * Revised code comments, reverted nullish coalesce * Add unit tests for nested immer produce statements Co-authored-by: Andrew Craswell <andcra@gmail.com> Co-authored-by: Jonas Holst Damtoft <Jomik@users.noreply.github.com>
1 parent a27a87f commit 9a00f7b

File tree

2 files changed

+51
-8
lines changed

2 files changed

+51
-8
lines changed

src/createReducer.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createReducer, CaseReducer } from './createReducer'
22
import { PayloadAction, createAction } from './createAction'
3+
import { createNextState, Draft } from './'
34
import { Reducer } from 'redux'
45

56
interface Todo {
@@ -75,6 +76,36 @@ describe('createReducer', () => {
7576
behavesLikeReducer(todosReducer)
7677
})
7778

79+
describe('given draft state from immer', () => {
80+
const addTodo: AddTodoReducer = (state, action) => {
81+
const { newTodo } = action.payload
82+
83+
// Can safely call state.push() here
84+
state.push({ ...newTodo, completed: false })
85+
}
86+
87+
const toggleTodo: ToggleTodoReducer = (state, action) => {
88+
const { index } = action.payload
89+
90+
const todo = state[index]
91+
// Can directly modify the todo object
92+
todo.completed = !todo.completed
93+
}
94+
95+
const todosReducer = createReducer([] as TodoState, {
96+
ADD_TODO: addTodo,
97+
TOGGLE_TODO: toggleTodo
98+
})
99+
100+
const wrappedReducer: TodosReducer = (state = [], action) => {
101+
return createNextState(state, (draft: Draft<TodoState>) => {
102+
todosReducer(draft, action)
103+
})
104+
}
105+
106+
behavesLikeReducer(wrappedReducer)
107+
})
108+
78109
describe('alternative builder callback for actionMap', () => {
79110
const increment = createAction<number, 'increment'>('increment')
80111
const decrement = createAction<number, 'decrement'>('decrement')

src/createReducer.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import createNextState, { Draft } from 'immer'
1+
import createNextState, { Draft, isDraft } from 'immer'
22
import { AnyAction, Action, Reducer } from 'redux'
33
import {
44
executeReducerBuilderCallback,
@@ -105,12 +105,24 @@ export function createReducer<S>(
105105
: mapOrBuilderCallback
106106

107107
return function(state = initialState, action): S {
108-
// @ts-ignore createNextState() produces an Immutable<Draft<S>> rather
109-
// than an Immutable<S>, and TypeScript cannot find out how to reconcile
110-
// these two types.
111-
return createNextState(state, (draft: Draft<S>) => {
112-
const caseReducer = actionsMap[action.type]
113-
return caseReducer ? caseReducer(draft, action) : undefined
114-
})
108+
const caseReducer = actionsMap[action.type]
109+
if (caseReducer) {
110+
if (isDraft(state)) {
111+
// we must already be inside a `createNextState` call, likely because
112+
// this is being wrapped in `createReducer`, `createSlice`, or nested
113+
// inside an existing draft. It's safe to just pass the draft to the mutator.
114+
const draft = state as Draft<S> // We can aassume this is already a draft
115+
return caseReducer(draft, action) || state
116+
} else {
117+
// @ts-ignore createNextState() produces an Immutable<Draft<S>> rather
118+
// than an Immutable<S>, and TypeScript cannot find out how to reconcile
119+
// these two types.
120+
return createNextState(state, (draft: Draft<S>) => {
121+
return caseReducer(draft, action)
122+
})
123+
}
124+
}
125+
126+
return state
115127
}
116128
}

0 commit comments

Comments
 (0)