Skip to content

combineSlices implementation #3297

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 55 commits into from
Apr 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
3c3bc4d
experiment with API typing
EskiMojo14 Mar 26, 2023
a50c968
support reducer map objects
EskiMojo14 Mar 26, 2023
00762a2
Prevent undeclared keys in reducer maps
EskiMojo14 Mar 26, 2023
5b81a3a
basic implementation
EskiMojo14 Mar 26, 2023
1397a52
export from entry point
EskiMojo14 Mar 26, 2023
075811e
rm inline typetest
EskiMojo14 Mar 26, 2023
99945eb
throw dev error if new reducer is injected when one already exists (w…
EskiMojo14 Mar 26, 2023
60d403b
typetest
EskiMojo14 Mar 26, 2023
7b4d8e0
more typetests
EskiMojo14 Mar 26, 2023
b3d4164
implement suggestions
EskiMojo14 Mar 26, 2023
818c595
add tests and fix oversight in injectSlices
EskiMojo14 Mar 26, 2023
6f1cbdf
use a Proxy to ensure injected reducers have state in selector
EskiMojo14 Mar 27, 2023
42db406
test result of selector instead of inside selector
EskiMojo14 Mar 27, 2023
b5db06a
syntax
EskiMojo14 Mar 27, 2023
8d5c282
add selectState parameter to handle nested reducers
Mar 27, 2023
b743008
use type assertion instead of ts-ignore
Mar 27, 2023
8ad0eb5
some JSDoc
Mar 27, 2023
a918ebd
handle RTKQ instances
Mar 27, 2023
bfdb10f
define original once and just attach to selector
Mar 27, 2023
8fc0d5d
throw an error when reducer returns undefined when called in state proxy
Mar 27, 2023
0c0d116
cache proxy creation, and use a proxy to mark a reducer as replaceable
EskiMojo14 Mar 27, 2023
327577f
export markReplaceable from package
EskiMojo14 Mar 27, 2023
43eb309
only allow injection of one slice/api at a time, and add config to al…
Mar 28, 2023
891a9f0
rename StaticState to InitialState, now it can be injected into
Mar 28, 2023
7058b93
injectSlice -> inject, Slice -> SliceLike, Api -> ApiLike, allowRepla…
Mar 28, 2023
cdb1e00
match RTKQ overrideExisting behaviour
Mar 28, 2023
7cdaf5b
fix test to match new behaviour
Mar 28, 2023
bac5361
remove unused export
Mar 28, 2023
7d7616c
no longer require slice to be declared before injection
Apr 2, 2023
157b88a
rework selector inference
Apr 2, 2023
1a218cf
check reducer matches
Apr 2, 2023
bd5d4ff
begin experimenting with slice selectors
Apr 6, 2023
c36c9dc
tests
Apr 6, 2023
d1ae818
default to ID function for getSelectors without selectState
Apr 6, 2023
deecec7
cache selectors
Apr 6, 2023
2731434
rm TODO
Apr 6, 2023
792c944
injectInto implementation
Apr 6, 2023
36503dc
more accurate typing
Apr 6, 2023
4e4a3a1
pass arguments through
Apr 6, 2023
32d1964
more tests
Apr 6, 2023
34bf78b
remove index signature from selectors generic
Apr 6, 2023
bccf47f
Merge pull request #1 from EskiMojo14/combine-slices-integrated
EskiMojo14 Apr 6, 2023
6816c1a
remove constraint on lazy
Apr 7, 2023
d59b26a
add optional name parameter for injectInto
Apr 7, 2023
a285bf8
adjust slice option
Apr 7, 2023
af875ae
add inject config to injectInto
Apr 7, 2023
e3e8c2d
JSDoc
Apr 7, 2023
ef07e12
custom name test
Apr 7, 2023
37ab272
add reducerPath option to slice
Apr 7, 2023
e92c7db
fix tests
Apr 7, 2023
737d5cc
simplify injectInto implementation
Apr 7, 2023
e52f609
throw if selectState returns undefined in an uninjected slice
Apr 7, 2023
0a23a5c
rm WithApi export
Apr 7, 2023
fbf8c24
don't throw error in production
Apr 8, 2023
8b2c55d
delete injectableCombineReducers.example.ts
Apr 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
446 changes: 446 additions & 0 deletions packages/toolkit/src/combineSlices.ts

Large diffs are not rendered by default.

194 changes: 185 additions & 9 deletions packages/toolkit/src/createSlice.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { AnyAction, Reducer } from 'redux'
import { createNextState } from '.'
import type {
ActionCreatorWithoutPayload,
PayloadAction,
Expand All @@ -16,8 +15,9 @@ import type {
import { createReducer, NotFunction } from './createReducer'
import type { ActionReducerMapBuilder } from './mapBuilders'
import { executeReducerBuilderCallback } from './mapBuilders'
import type { NoInfer } from './tsHelpers'
import type { Id, NoInfer, Tail } from './tsHelpers'
import { freezeDraftable } from './utils'
import type { CombinedSliceReducer, InjectConfig } from './combineSlices'

let hasWarnedAboutObjectNotation = false

Expand All @@ -38,13 +38,20 @@ export type SliceActionCreator<P> = PayloadActionCreator<P>
export interface Slice<
State = any,
CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>,
Name extends string = string
Name extends string = string,
ReducerPath extends string = Name,
Selectors extends SliceSelectors<State> = SliceSelectors<State>
> {
/**
* The slice name.
*/
name: Name

/**
* The slice reducer path.
*/
reducerPath: ReducerPath

/**
* The slice's reducer.
*/
Expand All @@ -67,6 +74,76 @@ export interface Slice<
* If a lazy state initializer was provided, it will be called and a fresh value returned.
*/
getInitialState: () => State

/**
* Get localised slice selectors (expects to be called with *just* the slice's state as the first parameter)
*/
getSelectors(): Id<SliceDefinedSelectors<State, Selectors, State>>

/**
* Get globalised slice selectors (`selectState` callback is expected to receive first parameter and return slice state)
*/
getSelectors<RootState>(
selectState: (rootState: RootState) => State
): Id<SliceDefinedSelectors<State, Selectors, RootState>>

/**
* Selectors that assume the slice's state is `rootState[slice.reducerPath]` (which is usually the case)
*
* Equivalent to `slice.getSelectors((state: RootState) => state[slice.reducerPath])`.
*/
selectors: Id<
SliceDefinedSelectors<State, Selectors, { [K in ReducerPath]: State }>
>

/**
* Inject slice into provided reducer (return value from `combineSlices`), and return injected slice.
*/
injectInto(
combinedReducer: CombinedSliceReducer<any>,
config?: InjectConfig & { reducerPath?: string }
): InjectedSlice<State, CaseReducers, Name, ReducerPath, Selectors>
}

/**
* A slice after being called with `injectInto(reducer)`.
*
* Selectors can now be called with an `undefined` value, in which case they use the slice's initial state.
*/
interface InjectedSlice<
State = any,
CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>,
Name extends string = string,
ReducerPath extends string = Name,
Selectors extends SliceSelectors<State> = SliceSelectors<State>
> extends Omit<
Slice<State, CaseReducers, Name, ReducerPath, Selectors>,
'getSelectors' | 'selectors'
> {
/**
* Get localised slice selectors (expects to be called with *just* the slice's state as the first parameter)
*/
getSelectors(): Id<SliceDefinedSelectors<State, Selectors, State | undefined>>

/**
* Get globalised slice selectors (`selectState` callback is expected to receive first parameter and return slice state)
*/
getSelectors<RootState>(
selectState: (rootState: RootState) => State | undefined
): Id<SliceDefinedSelectors<State, Selectors, RootState>>

/**
* Selectors that assume the slice's state is `rootState[slice.name]` (which is usually the case)
*
* Equivalent to `slice.getSelectors((state: RootState) => state[slice.name])`.
*/
selectors: Id<
SliceDefinedSelectors<
State,
Selectors,
{ [K in ReducerPath]?: State | undefined }
>
>
}

/**
Expand All @@ -77,13 +154,20 @@ export interface Slice<
export interface CreateSliceOptions<
State = any,
CR extends SliceCaseReducers<State> = SliceCaseReducers<State>,
Name extends string = string
Name extends string = string,
ReducerPath extends string = Name,
Selectors extends SliceSelectors<State> = SliceSelectors<State>
> {
/**
* The slice's name. Used to namespace the generated action types.
*/
name: Name

/**
* The slice's reducer path. Used when injecting into a combined slice reducer.
*/
reducerPath?: ReducerPath

/**
* 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`.
*/
Expand Down Expand Up @@ -142,6 +226,11 @@ createSlice({
```
*/
extraReducers?: (builder: ActionReducerMapBuilder<NoInfer<State>>) => void

/**
* A map of selectors that receive the slice's state and any additional arguments, and return a result.
*/
selectors?: Selectors
}

/**
Expand All @@ -165,6 +254,13 @@ export type SliceCaseReducers<State> = {
| CaseReducerWithPrepare<State, PayloadAction<any, string, any, any>>
}

/**
* The type describing a slice's `selectors` option.
*/
export type SliceSelectors<State> = {
[K: string]: (sliceState: State, ...args: any[]) => any
}

type SliceActionType<
SliceName extends string,
ActionName extends keyof any
Expand Down Expand Up @@ -228,6 +324,22 @@ type SliceDefinedCaseReducers<CaseReducers extends SliceCaseReducers<any>> = {
: CaseReducers[Type]
}

/**
* Extracts the final selector type from the `selectors` object.
*
* Removes the `string` index signature from the default value.
*/
type SliceDefinedSelectors<
State,
Selectors extends SliceSelectors<State>,
RootState
> = {
[K in keyof Selectors as string extends K ? never : K]: (
rootState: RootState,
...args: Tail<Parameters<Selectors[K]>>
) => ReturnType<Selectors[K]>
}

/**
* Used on a SliceCaseReducers object.
* Ensures that if a CaseReducer is a `CaseReducerWithPrepare`, that
Expand Down Expand Up @@ -271,11 +383,13 @@ function getType(slice: string, actionKey: string): string {
export function createSlice<
State,
CaseReducers extends SliceCaseReducers<State>,
Name extends string = string
Name extends string,
Selectors extends SliceSelectors<State>,
ReducerPath extends string = Name
>(
options: CreateSliceOptions<State, CaseReducers, Name>
): Slice<State, CaseReducers, Name> {
const { name } = options
options: CreateSliceOptions<State, CaseReducers, Name, ReducerPath, Selectors>
): Slice<State, CaseReducers, Name, ReducerPath, Selectors> {
const { name, reducerPath = name as unknown as ReducerPath } = options
if (!name) {
throw new Error('`name` is a required option for createSlice')
}
Expand Down Expand Up @@ -357,10 +471,25 @@ export function createSlice<
})
}

const defaultSelectSlice = (
rootState: { [K in ReducerPath]: State }
): State => rootState[reducerPath]

const selectSelf = (state: State) => state

const injectedSelectorCache = new WeakMap<
Slice<State, CaseReducers, Name, ReducerPath, Selectors>,
WeakMap<
(rootState: any) => State | undefined,
Record<string, (rootState: any) => any>
>
>()

let _reducer: ReducerWithInitialState<State>

return {
const slice: Slice<State, CaseReducers, Name, ReducerPath, Selectors> = {
name,
reducerPath,
reducer(state, action) {
if (!_reducer) _reducer = buildReducer()

Expand All @@ -373,5 +502,52 @@ export function createSlice<

return _reducer.getInitialState()
},
getSelectors(selectState: (rootState: any) => State = selectSelf) {
let selectorCache = injectedSelectorCache.get(this)
if (!selectorCache) {
selectorCache = new WeakMap()
injectedSelectorCache.set(this, selectorCache)
}
let cached = selectorCache.get(selectState)
if (!cached) {
cached = {}
for (const [name, selector] of Object.entries(
options.selectors ?? {}
)) {
cached[name] = (rootState: any, ...args: any[]) => {
let sliceState = selectState(rootState)
if (typeof sliceState === 'undefined') {
// check if injectInto has been called
if (this !== slice) {
sliceState = this.getInitialState()
} else if (process.env.NODE_ENV !== 'production') {
throw new Error(
'selectState returned undefined for an uninjected slice reducer'
)
}
}
return selector(sliceState, ...args)
}
}
selectorCache.set(selectState, cached)
}
return cached as any
},
get selectors() {
return this.getSelectors(defaultSelectSlice)
},
injectInto(injectable, { reducerPath, ...config } = {}) {
injectable.inject(
{ reducerPath: reducerPath ?? this.reducerPath, reducer: this.reducer },
config
)
return {
...this,
get selectors() {
return this.getSelectors(defaultSelectSlice)
},
} as any
},
}
return slice
}
4 changes: 4 additions & 0 deletions packages/toolkit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,7 @@ export {
autoBatchEnhancer,
} from './autoBatchEnhancer'
export type { AutoBatchOptions } from './autoBatchEnhancer'

export { combineSlices } from './combineSlices'

export type { WithSlice } from './combineSlices'
Loading