-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Closed
Milestone
Description
I'm just writing this down so I get my head emptied out and see if what I'm thinking makes any sense
reducer.ts
import { combineSlices } from '@reduxjs/toolkit'
import { sliceA } from 'fileA'
import { sliceB } from 'fileB'
import { lazySliceC } from 'fileC'
import type { lazySliceD } from 'fileD'
import { anotherReducer } from 'somewhere'
export interface LazyLoadedSlices {}
export const rootReducer = combineSlices(sliceA, sliceB, {
another: anotherReducer,
}).withLazyLoadedSlices<LazyLoadedSlices>()
/*
results in a return type of
{
[sliceA.name]: SliceAState,
[sliceB.name]: SliceBState,
another: AnotherState,
[lazySliceC.name]?: SliceCState, // see fileC.ts to understand why this appears here
[lazySliceD.name]?: SliceDState, // see fileD.ts to understand why this appears here
}
*/
the "naive" approach
fileC.ts
import { rootReducer, RootState } from './reducer'
import { createSlice } from '@reduxjs/toolkit'
interface SliceCState {
foo: string
}
declare module './reducer' {
export interface LazyLoadedSlices {
[lazySliceC.name]: SliceCState
}
}
export const lazySliceC = createSlice({
/* ... */
})
/**
* Synchronously call `injectSlice` in file.
* This will add `lazySliceC.reducer` to `rootReducer`, but **not** trigger an action.
* `state.lazySliceC` will stay `undefined` until the next action is dispatched.
*/
rootReducer.injectSlice(lazySliceC)
// this will still error - `lazySliceC` is optional here
const naiveSelectFoo = (state: RootState) => state.lazySliceC.foo
/**
* `injectSlice` would not inject the slice again if it is referentially equal, so it could be called
* in multiple files to get a `rootReducer` with types that are aware that `lazySliceC` has already
* been injected
*/
const selectFoo = rootReducer.injectSlice(lazySliceC).selector((state) => {
/**
* `lazySlice` is guaranteed to not be `undefined` here.
* we wrap the selector and if it is called before another action has been dispatched
* (meaning `state.lazySlice` is still `undefined`), the selector will not be called with
* the real `state`, but with a modified copy (or a Proxy or whatever we can do here to keep
* it as stable as possible)
*/
return state.lazySlice.foo
})
the next step (maybe in a later release?)
"integrated" approach - adding a selectors
field to createSlice
fileD.ts
import { rootReducer } from './reducer'
import { createSlice, WithSlice } from '@reduxjs/toolkit'
interface SliceDState {
bar: string
}
declare module './reducer' {
// new helper `WithSlice`
export interface LazyLoadedSlices extends WithSlice<typeof lazySliceD> {}
}
export const _lazySliceD = createSlice({
/* ... */
selectors: {
uppercaseBar: sliceState => sliceState.bar.uppercase()
}
})
// this will still error, as `state.lazySliceD` is still optional at this point
_lazySliceD.selectors.uppercaseBar(state)
// this will internally just call `rootReducer.injectSlice(_lazySliceD)
const lazySliceD = _lazySliceD.injectInto(rootReducer)
// this will now work even if no action has been dispatched yet, as `selectors` now have been wrapped
// in a similar approach to `rootReducer.withSlice(lazySliceC).selector`
lazySliceD.selectors.uppercaseBar(state)
general interface
declare function combineSlices(...slices: Slice | Record<string, reducer>): Reducer<CombinedState> & {
withLazyLoadedSlices<LazyLoadedSlices>() // returns same object with enhanced types - those slice states are now optional
injectSlice(slice: Slice) // returns same object with enhanced types - that slice's state is now non-optional
selector(selectorFn)
}
// additions to a `slice`:
injectInto(inectableReducer)
// additions to slice options:
selectors: Record<string, SelectorFn>
// this might have room for improvement - do we allow for selectors that also take other state parts into account here or do those need to be created outside? How do we allow for memoized selectors?
Open questions:
- do we have any circular type problems anywhere here?
- will this work nicely with nested
combineSlice
calls?
airjp73, orenklein and matinrcoTamasSzigetimatinrcoBardiamist
Metadata
Metadata
Assignees
Labels
No labels