Skip to content

Commit 34db089

Browse files
authored
Merge pull request #3297 from EskiMojo14/combine-slices
2 parents ac6aeb1 + 8b2c55d commit 34db089

File tree

9 files changed

+1196
-74
lines changed

9 files changed

+1196
-74
lines changed

packages/toolkit/src/combineSlices.ts

Lines changed: 446 additions & 0 deletions
Large diffs are not rendered by default.

packages/toolkit/src/createSlice.ts

Lines changed: 185 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { AnyAction, Reducer } from 'redux'
2-
import { createNextState } from '.'
32
import type {
43
ActionCreatorWithoutPayload,
54
PayloadAction,
@@ -16,8 +15,9 @@ import type {
1615
import { createReducer, NotFunction } from './createReducer'
1716
import type { ActionReducerMapBuilder } from './mapBuilders'
1817
import { executeReducerBuilderCallback } from './mapBuilders'
19-
import type { NoInfer } from './tsHelpers'
18+
import type { Id, NoInfer, Tail } from './tsHelpers'
2019
import { freezeDraftable } from './utils'
20+
import type { CombinedSliceReducer, InjectConfig } from './combineSlices'
2121

2222
let hasWarnedAboutObjectNotation = false
2323

@@ -38,13 +38,20 @@ export type SliceActionCreator<P> = PayloadActionCreator<P>
3838
export interface Slice<
3939
State = any,
4040
CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>,
41-
Name extends string = string
41+
Name extends string = string,
42+
ReducerPath extends string = Name,
43+
Selectors extends SliceSelectors<State> = SliceSelectors<State>
4244
> {
4345
/**
4446
* The slice name.
4547
*/
4648
name: Name
4749

50+
/**
51+
* The slice reducer path.
52+
*/
53+
reducerPath: ReducerPath
54+
4855
/**
4956
* The slice's reducer.
5057
*/
@@ -67,6 +74,76 @@ export interface Slice<
6774
* If a lazy state initializer was provided, it will be called and a fresh value returned.
6875
*/
6976
getInitialState: () => State
77+
78+
/**
79+
* Get localised slice selectors (expects to be called with *just* the slice's state as the first parameter)
80+
*/
81+
getSelectors(): Id<SliceDefinedSelectors<State, Selectors, State>>
82+
83+
/**
84+
* Get globalised slice selectors (`selectState` callback is expected to receive first parameter and return slice state)
85+
*/
86+
getSelectors<RootState>(
87+
selectState: (rootState: RootState) => State
88+
): Id<SliceDefinedSelectors<State, Selectors, RootState>>
89+
90+
/**
91+
* Selectors that assume the slice's state is `rootState[slice.reducerPath]` (which is usually the case)
92+
*
93+
* Equivalent to `slice.getSelectors((state: RootState) => state[slice.reducerPath])`.
94+
*/
95+
selectors: Id<
96+
SliceDefinedSelectors<State, Selectors, { [K in ReducerPath]: State }>
97+
>
98+
99+
/**
100+
* Inject slice into provided reducer (return value from `combineSlices`), and return injected slice.
101+
*/
102+
injectInto(
103+
combinedReducer: CombinedSliceReducer<any>,
104+
config?: InjectConfig & { reducerPath?: string }
105+
): InjectedSlice<State, CaseReducers, Name, ReducerPath, Selectors>
106+
}
107+
108+
/**
109+
* A slice after being called with `injectInto(reducer)`.
110+
*
111+
* Selectors can now be called with an `undefined` value, in which case they use the slice's initial state.
112+
*/
113+
interface InjectedSlice<
114+
State = any,
115+
CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>,
116+
Name extends string = string,
117+
ReducerPath extends string = Name,
118+
Selectors extends SliceSelectors<State> = SliceSelectors<State>
119+
> extends Omit<
120+
Slice<State, CaseReducers, Name, ReducerPath, Selectors>,
121+
'getSelectors' | 'selectors'
122+
> {
123+
/**
124+
* Get localised slice selectors (expects to be called with *just* the slice's state as the first parameter)
125+
*/
126+
getSelectors(): Id<SliceDefinedSelectors<State, Selectors, State | undefined>>
127+
128+
/**
129+
* Get globalised slice selectors (`selectState` callback is expected to receive first parameter and return slice state)
130+
*/
131+
getSelectors<RootState>(
132+
selectState: (rootState: RootState) => State | undefined
133+
): Id<SliceDefinedSelectors<State, Selectors, RootState>>
134+
135+
/**
136+
* Selectors that assume the slice's state is `rootState[slice.name]` (which is usually the case)
137+
*
138+
* Equivalent to `slice.getSelectors((state: RootState) => state[slice.name])`.
139+
*/
140+
selectors: Id<
141+
SliceDefinedSelectors<
142+
State,
143+
Selectors,
144+
{ [K in ReducerPath]?: State | undefined }
145+
>
146+
>
70147
}
71148

72149
/**
@@ -77,13 +154,20 @@ export interface Slice<
77154
export interface CreateSliceOptions<
78155
State = any,
79156
CR extends SliceCaseReducers<State> = SliceCaseReducers<State>,
80-
Name extends string = string
157+
Name extends string = string,
158+
ReducerPath extends string = Name,
159+
Selectors extends SliceSelectors<State> = SliceSelectors<State>
81160
> {
82161
/**
83162
* The slice's name. Used to namespace the generated action types.
84163
*/
85164
name: Name
86165

166+
/**
167+
* The slice's reducer path. Used when injecting into a combined slice reducer.
168+
*/
169+
reducerPath?: ReducerPath
170+
87171
/**
88172
* The initial state that should be used when the reducer is called the first time. This may also be a "lazy initializer" function, which should return an initial state value when called. This will be used whenever the reducer is called with `undefined` as its state value, and is primarily useful for cases like reading initial state from `localStorage`.
89173
*/
@@ -139,6 +223,11 @@ createSlice({
139223
```
140224
*/
141225
extraReducers?: (builder: ActionReducerMapBuilder<NoInfer<State>>) => void
226+
227+
/**
228+
* A map of selectors that receive the slice's state and any additional arguments, and return a result.
229+
*/
230+
selectors?: Selectors
142231
}
143232

144233
/**
@@ -162,6 +251,13 @@ export type SliceCaseReducers<State> = {
162251
| CaseReducerWithPrepare<State, PayloadAction<any, string, any, any>>
163252
}
164253

254+
/**
255+
* The type describing a slice's `selectors` option.
256+
*/
257+
export type SliceSelectors<State> = {
258+
[K: string]: (sliceState: State, ...args: any[]) => any
259+
}
260+
165261
type SliceActionType<
166262
SliceName extends string,
167263
ActionName extends keyof any
@@ -225,6 +321,22 @@ type SliceDefinedCaseReducers<CaseReducers extends SliceCaseReducers<any>> = {
225321
: CaseReducers[Type]
226322
}
227323

324+
/**
325+
* Extracts the final selector type from the `selectors` object.
326+
*
327+
* Removes the `string` index signature from the default value.
328+
*/
329+
type SliceDefinedSelectors<
330+
State,
331+
Selectors extends SliceSelectors<State>,
332+
RootState
333+
> = {
334+
[K in keyof Selectors as string extends K ? never : K]: (
335+
rootState: RootState,
336+
...args: Tail<Parameters<Selectors[K]>>
337+
) => ReturnType<Selectors[K]>
338+
}
339+
228340
/**
229341
* Used on a SliceCaseReducers object.
230342
* Ensures that if a CaseReducer is a `CaseReducerWithPrepare`, that
@@ -268,11 +380,13 @@ function getType(slice: string, actionKey: string): string {
268380
export function createSlice<
269381
State,
270382
CaseReducers extends SliceCaseReducers<State>,
271-
Name extends string = string
383+
Name extends string,
384+
Selectors extends SliceSelectors<State>,
385+
ReducerPath extends string = Name
272386
>(
273-
options: CreateSliceOptions<State, CaseReducers, Name>
274-
): Slice<State, CaseReducers, Name> {
275-
const { name } = options
387+
options: CreateSliceOptions<State, CaseReducers, Name, ReducerPath, Selectors>
388+
): Slice<State, CaseReducers, Name, ReducerPath, Selectors> {
389+
const { name, reducerPath = name as unknown as ReducerPath } = options
276390
if (!name) {
277391
throw new Error('`name` is a required option for createSlice')
278392
}
@@ -354,10 +468,25 @@ export function createSlice<
354468
})
355469
}
356470

471+
const defaultSelectSlice = (
472+
rootState: { [K in ReducerPath]: State }
473+
): State => rootState[reducerPath]
474+
475+
const selectSelf = (state: State) => state
476+
477+
const injectedSelectorCache = new WeakMap<
478+
Slice<State, CaseReducers, Name, ReducerPath, Selectors>,
479+
WeakMap<
480+
(rootState: any) => State | undefined,
481+
Record<string, (rootState: any) => any>
482+
>
483+
>()
484+
357485
let _reducer: ReducerWithInitialState<State>
358486

359-
return {
487+
const slice: Slice<State, CaseReducers, Name, ReducerPath, Selectors> = {
360488
name,
489+
reducerPath,
361490
reducer(state, action) {
362491
if (!_reducer) _reducer = buildReducer()
363492

@@ -370,5 +499,52 @@ export function createSlice<
370499

371500
return _reducer.getInitialState()
372501
},
502+
getSelectors(selectState: (rootState: any) => State = selectSelf) {
503+
let selectorCache = injectedSelectorCache.get(this)
504+
if (!selectorCache) {
505+
selectorCache = new WeakMap()
506+
injectedSelectorCache.set(this, selectorCache)
507+
}
508+
let cached = selectorCache.get(selectState)
509+
if (!cached) {
510+
cached = {}
511+
for (const [name, selector] of Object.entries(
512+
options.selectors ?? {}
513+
)) {
514+
cached[name] = (rootState: any, ...args: any[]) => {
515+
let sliceState = selectState(rootState)
516+
if (typeof sliceState === 'undefined') {
517+
// check if injectInto has been called
518+
if (this !== slice) {
519+
sliceState = this.getInitialState()
520+
} else if (process.env.NODE_ENV !== 'production') {
521+
throw new Error(
522+
'selectState returned undefined for an uninjected slice reducer'
523+
)
524+
}
525+
}
526+
return selector(sliceState, ...args)
527+
}
528+
}
529+
selectorCache.set(selectState, cached)
530+
}
531+
return cached as any
532+
},
533+
get selectors() {
534+
return this.getSelectors(defaultSelectSlice)
535+
},
536+
injectInto(injectable, { reducerPath, ...config } = {}) {
537+
injectable.inject(
538+
{ reducerPath: reducerPath ?? this.reducerPath, reducer: this.reducer },
539+
config
540+
)
541+
return {
542+
...this,
543+
get selectors() {
544+
return this.getSelectors(defaultSelectSlice)
545+
},
546+
} as any
547+
},
373548
}
549+
return slice
374550
}

packages/toolkit/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,7 @@ export {
186186
autoBatchEnhancer,
187187
} from './autoBatchEnhancer'
188188
export type { AutoBatchOptions } from './autoBatchEnhancer'
189+
190+
export { combineSlices } from './combineSlices'
191+
192+
export type { WithSlice } from './combineSlices'

0 commit comments

Comments
 (0)