Skip to content

Commit 8dec012

Browse files
committed
add unwrapped property to wrapped selectors
1 parent bce3371 commit 8dec012

File tree

3 files changed

+59
-23
lines changed

3 files changed

+59
-23
lines changed

packages/toolkit/src/createSlice.ts

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,14 @@ type SliceDefinedCaseReducers<CaseReducers extends SliceCaseReducers<any>> = {
533533
: never
534534
}
535535

536+
type RemappedSelector<S extends Selector, NewState> = S extends Selector<
537+
any,
538+
infer R,
539+
infer P
540+
>
541+
? Selector<NewState, R, P> & { unwrapped: S }
542+
: never
543+
536544
/**
537545
* Extracts the final selector type from the `selectors` object.
538546
*
@@ -543,10 +551,10 @@ type SliceDefinedSelectors<
543551
Selectors extends SliceSelectors<State>,
544552
RootState
545553
> = {
546-
[K in keyof Selectors as string extends K ? never : K]: (
547-
rootState: RootState,
548-
...args: Tail<Parameters<Selectors[K]>>
549-
) => ReturnType<Selectors[K]>
554+
[K in keyof Selectors as string extends K ? never : K]: RemappedSelector<
555+
Selectors[K],
556+
RootState
557+
>
550558
}
551559

552560
/**
@@ -768,20 +776,12 @@ export function buildCreateSlice({ creators }: BuildCreateSliceConfig = {}) {
768776
for (const [name, selector] of Object.entries(
769777
options.selectors ?? {}
770778
)) {
771-
map[name] = (rootState: any, ...args: any[]) => {
772-
let sliceState = selectState.call(this, rootState)
773-
if (typeof sliceState === 'undefined') {
774-
// check if injectInto has been called
775-
if (this !== slice) {
776-
sliceState = this.getInitialState()
777-
} else if (process.env.NODE_ENV !== 'production') {
778-
throw new Error(
779-
'selectState returned undefined for an uninjected slice reducer'
780-
)
781-
}
782-
}
783-
return selector(sliceState, ...args)
784-
}
779+
map[name] = wrapSelector(
780+
this,
781+
selector,
782+
selectState,
783+
this !== slice
784+
)
785785
}
786786
return map
787787
},
@@ -817,6 +817,29 @@ export function buildCreateSlice({ creators }: BuildCreateSliceConfig = {}) {
817817
}
818818
}
819819

820+
function wrapSelector<State, NewState, S extends Selector<State>>(
821+
slice: Slice,
822+
selector: S,
823+
selectState: Selector<NewState, State>,
824+
injected?: boolean
825+
) {
826+
function wrapper(rootState: NewState, ...args: any[]) {
827+
let sliceState = selectState.call(slice, rootState)
828+
if (typeof sliceState === 'undefined') {
829+
if (injected) {
830+
sliceState = slice.getInitialState()
831+
} else if (process.env.NODE_ENV !== 'production') {
832+
throw new Error(
833+
'selectState returned undefined for an uninjected slice reducer'
834+
)
835+
}
836+
}
837+
return selector(sliceState, ...args)
838+
}
839+
wrapper.unwrapped = selector
840+
return wrapper as RemappedSelector<S, NewState>
841+
}
842+
820843
/**
821844
* A function that accepts an initial state, an object full of reducer
822845
* functions, and a "slice name", and automatically generates

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -466,12 +466,15 @@ describe('createSlice', () => {
466466
reducers: {},
467467
selectors: {
468468
selectSlice: (state) => state,
469-
selectMultiple: (state, multiplier: number) => state * multiplier,
469+
selectMultiple: Object.assign(
470+
(state: number, multiplier: number) => state * multiplier,
471+
{ test: 0 }
472+
),
470473
},
471474
})
472-
it('expects reducer under slice.name if no selectState callback passed', () => {
475+
it('expects reducer under slice.reducerPath if no selectState callback passed', () => {
473476
const testState = {
474-
[slice.name]: slice.getInitialState(),
477+
[slice.reducerPath]: slice.getInitialState(),
475478
}
476479
const { selectSlice, selectMultiple } = slice.selectors
477480
expect(selectSlice(testState)).toBe(slice.getInitialState())
@@ -487,6 +490,9 @@ describe('createSlice', () => {
487490
expect(selectSlice(customState)).toBe(slice.getInitialState())
488491
expect(selectMultiple(customState, 2)).toBe(slice.getInitialState() * 2)
489492
})
493+
it('allows accessing properties on the selector', () => {
494+
expect(slice.selectors.selectMultiple.unwrapped.test).toBe(0)
495+
})
490496
})
491497
describe('slice injections', () => {
492498
it('uses injectInto to inject slice into combined reducer', () => {
@@ -580,7 +586,9 @@ describe('createSlice', () => {
580586
initialState: [] as any[],
581587
reducers: (create) => ({ thunk: create.asyncThunk(() => {}) }),
582588
})
583-
).toThrowErrorMatchingInlineSnapshot('"Cannot use `create.asyncThunk` in the built-in `createSlice`. Use `buildCreateSlice({ creators: { asyncThunk: asyncThunkCreator } })` to create a customised version of `createSlice`."')
589+
).toThrowErrorMatchingInlineSnapshot(
590+
'"Cannot use `create.asyncThunk` in the built-in `createSlice`. Use `buildCreateSlice({ creators: { asyncThunk: asyncThunkCreator } })` to create a customised version of `createSlice`."'
591+
)
584592
})
585593
const createThunkSlice = buildCreateSlice({
586594
creators: { asyncThunk: asyncThunkCreator },

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,10 @@ const value = actionCreators.anyKey
540540
selectors: {
541541
selectValue: (state) => state.value,
542542
selectMultiply: (state, multiplier: number) => state.value * multiplier,
543-
selectToFixed: (state) => state.value.toFixed(2),
543+
selectToFixed: Object.assign(
544+
(state: { value: number }) => state.value.toFixed(2),
545+
{ static: true }
546+
),
544547
},
545548
})
546549

@@ -555,6 +558,8 @@ const value = actionCreators.anyKey
555558
expectType<number>(selectMultiply(rootState, 2))
556559
expectType<string>(selectToFixed(rootState))
557560

561+
expectType<boolean>(selectToFixed.unwrapped.static)
562+
558563
const nestedState = {
559564
nested: rootState,
560565
}

0 commit comments

Comments
 (0)