1
1
import type { AnyAction , Reducer } from 'redux'
2
- import { createNextState } from '.'
3
2
import type {
4
3
ActionCreatorWithoutPayload ,
5
4
PayloadAction ,
@@ -16,8 +15,9 @@ import type {
16
15
import { createReducer , NotFunction } from './createReducer'
17
16
import type { ActionReducerMapBuilder } from './mapBuilders'
18
17
import { executeReducerBuilderCallback } from './mapBuilders'
19
- import type { NoInfer } from './tsHelpers'
18
+ import type { Id , NoInfer , Tail } from './tsHelpers'
20
19
import { freezeDraftable } from './utils'
20
+ import type { CombinedSliceReducer , InjectConfig } from './combineSlices'
21
21
22
22
let hasWarnedAboutObjectNotation = false
23
23
@@ -38,13 +38,20 @@ export type SliceActionCreator<P> = PayloadActionCreator<P>
38
38
export interface Slice <
39
39
State = any ,
40
40
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 >
42
44
> {
43
45
/**
44
46
* The slice name.
45
47
*/
46
48
name : Name
47
49
50
+ /**
51
+ * The slice reducer path.
52
+ */
53
+ reducerPath : ReducerPath
54
+
48
55
/**
49
56
* The slice's reducer.
50
57
*/
@@ -67,6 +74,76 @@ export interface Slice<
67
74
* If a lazy state initializer was provided, it will be called and a fresh value returned.
68
75
*/
69
76
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
+ >
70
147
}
71
148
72
149
/**
@@ -77,13 +154,20 @@ export interface Slice<
77
154
export interface CreateSliceOptions <
78
155
State = any ,
79
156
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 >
81
160
> {
82
161
/**
83
162
* The slice's name. Used to namespace the generated action types.
84
163
*/
85
164
name : Name
86
165
166
+ /**
167
+ * The slice's reducer path. Used when injecting into a combined slice reducer.
168
+ */
169
+ reducerPath ?: ReducerPath
170
+
87
171
/**
88
172
* 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`.
89
173
*/
@@ -139,6 +223,11 @@ createSlice({
139
223
```
140
224
*/
141
225
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
142
231
}
143
232
144
233
/**
@@ -162,6 +251,13 @@ export type SliceCaseReducers<State> = {
162
251
| CaseReducerWithPrepare < State , PayloadAction < any , string , any , any > >
163
252
}
164
253
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
+
165
261
type SliceActionType <
166
262
SliceName extends string ,
167
263
ActionName extends keyof any
@@ -225,6 +321,22 @@ type SliceDefinedCaseReducers<CaseReducers extends SliceCaseReducers<any>> = {
225
321
: CaseReducers [ Type ]
226
322
}
227
323
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
+
228
340
/**
229
341
* Used on a SliceCaseReducers object.
230
342
* Ensures that if a CaseReducer is a `CaseReducerWithPrepare`, that
@@ -268,11 +380,13 @@ function getType(slice: string, actionKey: string): string {
268
380
export function createSlice <
269
381
State ,
270
382
CaseReducers extends SliceCaseReducers < State > ,
271
- Name extends string = string
383
+ Name extends string ,
384
+ Selectors extends SliceSelectors < State > ,
385
+ ReducerPath extends string = Name
272
386
> (
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
276
390
if ( ! name ) {
277
391
throw new Error ( '`name` is a required option for createSlice' )
278
392
}
@@ -354,10 +468,25 @@ export function createSlice<
354
468
} )
355
469
}
356
470
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
+
357
485
let _reducer : ReducerWithInitialState < State >
358
486
359
- return {
487
+ const slice : Slice < State , CaseReducers , Name , ReducerPath , Selectors > = {
360
488
name,
489
+ reducerPath,
361
490
reducer ( state , action ) {
362
491
if ( ! _reducer ) _reducer = buildReducer ( )
363
492
@@ -370,5 +499,52 @@ export function createSlice<
370
499
371
500
return _reducer . getInitialState ( )
372
501
} ,
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
+ } ,
373
548
}
549
+ return slice
374
550
}
0 commit comments