Skip to content

Commit

Permalink
allow for circular references by building reducer lazily on first red…
Browse files Browse the repository at this point in the history
…ucer call (#1686)
  • Loading branch information
phryneas committed Nov 4, 2021
1 parent 561645b commit c455fc2
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 23 deletions.
63 changes: 43 additions & 20 deletions packages/toolkit/src/createSlice.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Reducer } from 'redux'
import type { AnyAction, Reducer } from 'redux'
import { createNextState } from '.'
import type {
ActionCreatorWithoutPayload,
PayloadAction,
Expand All @@ -7,7 +8,11 @@ import type {
_ActionCreatorWithPreparedPayload,
} from './createAction'
import { createAction } from './createAction'
import type { CaseReducer, CaseReducers } from './createReducer'
import type {
CaseReducer,
CaseReducers,
ReducerWithInitialState,
} from './createReducer'
import { createReducer, NotFunction } from './createReducer'
import type { ActionReducerMapBuilder } from './mapBuilders'
import { executeReducerBuilderCallback } from './mapBuilders'
Expand Down Expand Up @@ -253,19 +258,16 @@ export function createSlice<
>(
options: CreateSliceOptions<State, CaseReducers, Name>
): Slice<State, CaseReducers, Name> {
const { name, initialState } = options
const { name } = options
if (!name) {
throw new Error('`name` is a required option for createSlice')
}
const initialState =
typeof options.initialState == 'function'
? options.initialState
: createNextState(options.initialState, () => {})

const reducers = options.reducers || {}
const [
extraReducers = {},
actionMatchers = [],
defaultCaseReducer = undefined,
] =
typeof options.extraReducers === 'function'
? executeReducerBuilderCallback(options.extraReducers)
: [options.extraReducers]

const reducerNames = Object.keys(reducers)

Expand Down Expand Up @@ -294,19 +296,40 @@ export function createSlice<
: createAction(type)
})

const finalCaseReducers = { ...extraReducers, ...sliceCaseReducersByType }
const reducer = createReducer(
initialState,
finalCaseReducers as any,
actionMatchers,
defaultCaseReducer
)
function buildReducer() {
const [
extraReducers = {},
actionMatchers = [],
defaultCaseReducer = undefined,
] =
typeof options.extraReducers === 'function'
? executeReducerBuilderCallback(options.extraReducers)
: [options.extraReducers]

const finalCaseReducers = { ...extraReducers, ...sliceCaseReducersByType }
return createReducer(
initialState,
finalCaseReducers as any,
actionMatchers,
defaultCaseReducer
)
}

let _reducer: ReducerWithInitialState<State>

return {
name,
reducer,
reducer(state, action) {
if (!_reducer) _reducer = buildReducer()

return _reducer(state, action)
},
actions: actionCreators as any,
caseReducers: sliceCaseReducersByName as any,
getInitialState: reducer.getInitialState,
getInitialState() {
if (!_reducer) _reducer = buildReducer()

return _reducer.getInitialState()
},
}
}
61 changes: 58 additions & 3 deletions packages/toolkit/src/tests/createSlice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,8 @@ describe('createSlice', () => {
})

test('prevents the same action type from being specified twice', () => {
expect(() =>
createSlice({
expect(() => {
const slice = createSlice({
name: 'counter',
initialState: 0,
reducers: {},
Expand All @@ -191,7 +191,8 @@ describe('createSlice', () => {
.addCase('increment', (state) => state + 1)
.addCase('increment', (state) => state + 1),
})
).toThrowErrorMatchingInlineSnapshot(
slice.reducer(undefined, { type: 'unrelated' })
}).toThrowErrorMatchingInlineSnapshot(
`"addCase cannot be called with two reducers for the same action type"`
)
})
Expand Down Expand Up @@ -269,4 +270,58 @@ describe('createSlice', () => {
)
})
})

describe('circularity', () => {
test('extraReducers can reference each other circularly', () => {
const first = createSlice({
name: 'first',
initialState: 'firstInitial',
reducers: {
something() {
return 'firstSomething'
},
},
extraReducers(builder) {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
builder.addCase(second.actions.other, () => {
return 'firstOther'
})
},
})
const second = createSlice({
name: 'second',
initialState: 'secondInitial',
reducers: {
other() {
return 'secondOther'
},
},
extraReducers(builder) {
builder.addCase(first.actions.something, () => {
return 'secondSomething'
})
},
})

expect(first.reducer(undefined, { type: 'unrelated' })).toBe(
'firstInitial'
)
expect(first.reducer(undefined, first.actions.something())).toBe(
'firstSomething'
)
expect(first.reducer(undefined, second.actions.other())).toBe(
'firstOther'
)

expect(second.reducer(undefined, { type: 'unrelated' })).toBe(
'secondInitial'
)
expect(second.reducer(undefined, first.actions.something())).toBe(
'secondSomething'
)
expect(second.reducer(undefined, second.actions.other())).toBe(
'secondOther'
)
})
})
})

0 comments on commit c455fc2

Please sign in to comment.