Skip to content

Commit 455809f

Browse files
committed
allow for circular references by building reducer lazily on first reducer call
1 parent 561645b commit 455809f

File tree

2 files changed

+97
-23
lines changed

2 files changed

+97
-23
lines changed

packages/toolkit/src/createSlice.ts

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { Reducer } from 'redux'
1+
import type { AnyAction, Reducer } from 'redux'
2+
import { createNextState } from '.'
23
import type {
34
ActionCreatorWithoutPayload,
45
PayloadAction,
@@ -7,7 +8,11 @@ import type {
78
_ActionCreatorWithPreparedPayload,
89
} from './createAction'
910
import { createAction } from './createAction'
10-
import type { CaseReducer, CaseReducers } from './createReducer'
11+
import type {
12+
CaseReducer,
13+
CaseReducers,
14+
ReducerWithInitialState,
15+
} from './createReducer'
1116
import { createReducer, NotFunction } from './createReducer'
1217
import type { ActionReducerMapBuilder } from './mapBuilders'
1318
import { executeReducerBuilderCallback } from './mapBuilders'
@@ -253,19 +258,16 @@ export function createSlice<
253258
>(
254259
options: CreateSliceOptions<State, CaseReducers, Name>
255260
): Slice<State, CaseReducers, Name> {
256-
const { name, initialState } = options
261+
const { name } = options
257262
if (!name) {
258263
throw new Error('`name` is a required option for createSlice')
259264
}
265+
const initialState =
266+
typeof options.initialState == 'function'
267+
? options.initialState
268+
: createNextState(options.initialState, () => {})
269+
260270
const reducers = options.reducers || {}
261-
const [
262-
extraReducers = {},
263-
actionMatchers = [],
264-
defaultCaseReducer = undefined,
265-
] =
266-
typeof options.extraReducers === 'function'
267-
? executeReducerBuilderCallback(options.extraReducers)
268-
: [options.extraReducers]
269271

270272
const reducerNames = Object.keys(reducers)
271273

@@ -294,19 +296,36 @@ export function createSlice<
294296
: createAction(type)
295297
})
296298

297-
const finalCaseReducers = { ...extraReducers, ...sliceCaseReducersByType }
298-
const reducer = createReducer(
299-
initialState,
300-
finalCaseReducers as any,
301-
actionMatchers,
302-
defaultCaseReducer
303-
)
299+
function buildReducer() {
300+
const [
301+
extraReducers = {},
302+
actionMatchers = [],
303+
defaultCaseReducer = undefined,
304+
] =
305+
typeof options.extraReducers === 'function'
306+
? executeReducerBuilderCallback(options.extraReducers)
307+
: [options.extraReducers]
308+
309+
const finalCaseReducers = { ...extraReducers, ...sliceCaseReducersByType }
310+
return createReducer(
311+
initialState,
312+
finalCaseReducers as any,
313+
actionMatchers,
314+
defaultCaseReducer
315+
)
316+
}
317+
318+
let _reducer: ReducerWithInitialState<State>
304319

305320
return {
306321
name,
307-
reducer,
322+
reducer(state, action) {
323+
return (_reducer ??= buildReducer())(state, action)
324+
},
308325
actions: actionCreators as any,
309326
caseReducers: sliceCaseReducersByName as any,
310-
getInitialState: reducer.getInitialState,
327+
getInitialState() {
328+
return (_reducer ??= buildReducer()).getInitialState()
329+
},
311330
}
312331
}

packages/toolkit/src/tests/createSlice.test.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,8 @@ describe('createSlice', () => {
181181
})
182182

183183
test('prevents the same action type from being specified twice', () => {
184-
expect(() =>
185-
createSlice({
184+
expect(() => {
185+
const slice = createSlice({
186186
name: 'counter',
187187
initialState: 0,
188188
reducers: {},
@@ -191,7 +191,8 @@ describe('createSlice', () => {
191191
.addCase('increment', (state) => state + 1)
192192
.addCase('increment', (state) => state + 1),
193193
})
194-
).toThrowErrorMatchingInlineSnapshot(
194+
slice.reducer(undefined, { type: 'unrelated' })
195+
}).toThrowErrorMatchingInlineSnapshot(
195196
`"addCase cannot be called with two reducers for the same action type"`
196197
)
197198
})
@@ -269,4 +270,58 @@ describe('createSlice', () => {
269270
)
270271
})
271272
})
273+
274+
describe('circularity', () => {
275+
test('extraReducers can reference each other circularly', () => {
276+
const first = createSlice({
277+
name: 'first',
278+
initialState: 'firstInitial',
279+
reducers: {
280+
something() {
281+
return 'firstSomething'
282+
},
283+
},
284+
extraReducers(builder) {
285+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
286+
builder.addCase(second.actions.other, () => {
287+
return 'firstOther'
288+
})
289+
},
290+
})
291+
const second = createSlice({
292+
name: 'second',
293+
initialState: 'secondInitial',
294+
reducers: {
295+
other() {
296+
return 'secondOther'
297+
},
298+
},
299+
extraReducers(builder) {
300+
builder.addCase(first.actions.something, () => {
301+
return 'secondSomething'
302+
})
303+
},
304+
})
305+
306+
expect(first.reducer(undefined, { type: 'unrelated' })).toBe(
307+
'firstInitial'
308+
)
309+
expect(first.reducer(undefined, first.actions.something())).toBe(
310+
'firstSomething'
311+
)
312+
expect(first.reducer(undefined, second.actions.other())).toBe(
313+
'firstOther'
314+
)
315+
316+
expect(second.reducer(undefined, { type: 'unrelated' })).toBe(
317+
'secondInitial'
318+
)
319+
expect(second.reducer(undefined, first.actions.something())).toBe(
320+
'secondSomething'
321+
)
322+
expect(second.reducer(undefined, second.actions.other())).toBe(
323+
'secondOther'
324+
)
325+
})
326+
})
272327
})

0 commit comments

Comments
 (0)