Skip to content

Commit 5691d88

Browse files
committed
Modify createStateOperator to detect and handle Immer drafts
1 parent 3b1e203 commit 5691d88

File tree

2 files changed

+78
-6
lines changed

2 files changed

+78
-6
lines changed

src/entities/state_adapter.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { createEntityAdapter, EntityAdapter } from './index'
2+
import { PayloadAction } from '../createAction'
3+
import { configureStore } from '../configureStore'
4+
import { createSlice } from '../createSlice'
5+
import { BookModel } from './fixtures/book'
6+
7+
describe('createStateOperator', () => {
8+
let adapter: EntityAdapter<BookModel>
9+
10+
beforeEach(() => {
11+
adapter = createEntityAdapter({
12+
selectId: (book: BookModel) => book.id
13+
})
14+
})
15+
it('Correctly mutates a draft state when inside `createNextState', () => {
16+
const booksSlice = createSlice({
17+
name: 'books',
18+
initialState: adapter.getInitialState(),
19+
reducers: {
20+
// We should be able to call an adapter method as a mutating helper in a larger reducer
21+
addOne(state, action: PayloadAction<BookModel>) {
22+
// Originally, having nested `produce` calls don't mutate `state` here as I would have expected.
23+
// (note that `state` here is actually an Immer Draft<S>, from `createReducer`)
24+
// One woarkound was to return the new plain result value instead
25+
// See https://github.com/immerjs/immer/issues/533
26+
// However, after tweaking `createStateOperator` to check if the argument is a draft,
27+
// we can just treat the operator as strictly mutating, without returning a result,
28+
// and the result should be correct.
29+
const result = adapter.addOne(state, action)
30+
expect(result.ids.length).toBe(1)
31+
//Deliberately _don't_ return result
32+
},
33+
// We should also be able to pass them individually as case reducers
34+
addAnother: adapter.addOne
35+
}
36+
})
37+
38+
const { addOne, addAnother } = booksSlice.actions
39+
40+
const store = configureStore({
41+
reducer: {
42+
books: booksSlice.reducer
43+
}
44+
})
45+
46+
const book1: BookModel = { id: 'a', title: 'First' }
47+
store.dispatch(addOne(book1))
48+
49+
const state1 = store.getState()
50+
expect(state1.books.ids.length).toBe(1)
51+
expect(state1.books.entities['a']).toBe(book1)
52+
53+
const book2: BookModel = { id: 'b', title: 'Second' }
54+
store.dispatch(addAnother(book2))
55+
56+
const state2 = store.getState()
57+
expect(state2.books.ids.length).toBe(2)
58+
expect(state2.books.entities['b']).toBe(book2)
59+
})
60+
})

src/entities/state_adapter.ts

Lines changed: 18 additions & 6 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 { EntityState } from './models'
33
import { PayloadAction, isFSA } from '../createAction'
44

@@ -12,15 +12,27 @@ export function createStateOperator<V, R>(
1212
state: any,
1313
arg: R | PayloadAction<R>
1414
): S {
15-
// @ts-ignore createNextState() produces an Immutable<Draft<S>> rather
16-
// than an Immutable<S>, and TypeScript cannot find out how to reconcile
17-
// these two types.
18-
return createNextState(state, (draft: Draft<EntityState<V>>) => {
15+
const runMutator = (draft: Draft<EntityState<V>>) => {
1916
if (isFSA(arg)) {
2017
mutator(arg.payload, draft)
2118
} else {
2219
mutator(arg, draft)
2320
}
24-
})
21+
}
22+
23+
if (isDraft(state)) {
24+
// we must already be inside a `createNextState` call, likely because
25+
// this is being wrapped in `createReducer` or `createSlice`.
26+
// It's safe to just pass the draft to the mutator.
27+
runMutator(state)
28+
29+
// since it's a draft, we'll just return it
30+
return state
31+
} else {
32+
// @ts-ignore createNextState() produces an Immutable<Draft<S>> rather
33+
// than an Immutable<S>, and TypeScript cannot find out how to reconcile
34+
// these two types.
35+
return createNextState(state, runMutator)
36+
}
2537
}
2638
}

0 commit comments

Comments
 (0)