Skip to content

Commit d82b454

Browse files
authoredMar 30, 2025
Cache initial state in injected scenarios (#4908)
* add cache to injected slices * add cache to combineSlices * avoid calling prop.toString so much * add tests for createSlice cache * add test for combineSlices cache
1 parent c539030 commit d82b454

File tree

4 files changed

+83
-5
lines changed

4 files changed

+83
-5
lines changed
 

‎packages/toolkit/src/combineSlices.ts

+15-3
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,8 @@ const stateProxyMap = new WeakMap<object, object>()
322322

323323
const createStateProxy = <State extends object>(
324324
state: State,
325-
reducerMap: Partial<Record<string, Reducer>>,
325+
reducerMap: Partial<Record<PropertyKey, Reducer>>,
326+
initialStateCache: Record<PropertyKey, unknown>,
326327
) =>
327328
getOrInsertComputed(
328329
stateProxyMap,
@@ -333,7 +334,9 @@ const createStateProxy = <State extends object>(
333334
if (prop === ORIGINAL_STATE) return target
334335
const result = Reflect.get(target, prop, receiver)
335336
if (typeof result === 'undefined') {
336-
const reducer = reducerMap[prop.toString()]
337+
const cached = initialStateCache[prop]
338+
if (typeof cached !== 'undefined') return cached
339+
const reducer = reducerMap[prop]
337340
if (reducer) {
338341
// ensure action type is random, to prevent reducer treating it differently
339342
const reducerResult = reducer(undefined, { type: nanoid() })
@@ -346,6 +349,7 @@ const createStateProxy = <State extends object>(
346349
`you can use null instead of undefined.`,
347350
)
348351
}
352+
initialStateCache[prop] = reducerResult
349353
return reducerResult
350354
}
351355
}
@@ -361,7 +365,8 @@ const original = (state: any) => {
361365
return state[ORIGINAL_STATE]
362366
}
363367

364-
const noopReducer: Reducer<Record<string, any>> = (state = {}) => state
368+
const emptyObject = {}
369+
const noopReducer: Reducer<Record<string, any>> = (state = emptyObject) => state
365370

366371
export function combineSlices<Slices extends Array<AnySliceLike | ReducerMap>>(
367372
...slices: Slices
@@ -382,6 +387,8 @@ export function combineSlices<Slices extends Array<AnySliceLike | ReducerMap>>(
382387

383388
combinedReducer.withLazyLoadedSlices = () => combinedReducer
384389

390+
const initialStateCache: Record<PropertyKey, unknown> = {}
391+
385392
const inject = (
386393
slice: AnySliceLike,
387394
config: InjectConfig = {},
@@ -406,6 +413,10 @@ export function combineSlices<Slices extends Array<AnySliceLike | ReducerMap>>(
406413
return combinedReducer
407414
}
408415

416+
if (config.overrideExisting && currentReducer !== reducerToInject) {
417+
delete initialStateCache[reducerPath]
418+
}
419+
409420
reducerMap[reducerPath] = reducerToInject
410421

411422
reducer = getReducer()
@@ -423,6 +434,7 @@ export function combineSlices<Slices extends Array<AnySliceLike | ReducerMap>>(
423434
createStateProxy(
424435
selectState ? selectState(state as any, ...args) : state,
425436
reducerMap,
437+
initialStateCache,
426438
),
427439
...args,
428440
)

‎packages/toolkit/src/createSlice.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,8 @@ export function buildCreateSlice({ creators }: BuildCreateSliceConfig = {}) {
732732
>
733733
>()
734734

735+
const injectedStateCache = new WeakMap<(rootState: any) => State, State>()
736+
735737
let _reducer: ReducerWithInitialState<State>
736738

737739
function reducer(state: State | undefined, action: UnknownAction) {
@@ -757,7 +759,11 @@ export function buildCreateSlice({ creators }: BuildCreateSliceConfig = {}) {
757759
let sliceState = state[reducerPath]
758760
if (typeof sliceState === 'undefined') {
759761
if (injected) {
760-
sliceState = getInitialState()
762+
sliceState = getOrInsertComputed(
763+
injectedStateCache,
764+
selectSlice,
765+
getInitialState,
766+
)
761767
} else if (process.env.NODE_ENV !== 'production') {
762768
throw new Error(
763769
'selectSlice returned undefined for an uninjected slice reducer',
@@ -766,6 +772,7 @@ export function buildCreateSlice({ creators }: BuildCreateSliceConfig = {}) {
766772
}
767773
return sliceState
768774
}
775+
769776
function getSelectors(
770777
selectState: (rootState: any) => State = selectSelf,
771778
) {
@@ -783,7 +790,12 @@ export function buildCreateSlice({ creators }: BuildCreateSliceConfig = {}) {
783790
map[name] = wrapSelector(
784791
selector,
785792
selectState,
786-
getInitialState,
793+
() =>
794+
getOrInsertComputed(
795+
injectedStateCache,
796+
selectState,
797+
getInitialState,
798+
),
787799
injected,
788800
)
789801
}

‎packages/toolkit/src/tests/combineSlices.test.ts

+22
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ const numberSlice = createSlice({
2222

2323
const booleanReducer = createReducer(false, () => {})
2424

25+
const counterReducer = createSlice({
26+
name: 'counter',
27+
initialState: () => ({ value: 0 }),
28+
reducers: {},
29+
})
30+
2531
// mimic - we can't use RTKQ here directly
2632
const api = {
2733
reducerPath: 'api' as const,
@@ -144,6 +150,7 @@ describe('combineSlices', () => {
144150
describe('selector', () => {
145151
const combinedReducer = combineSlices(stringSlice).withLazyLoadedSlices<{
146152
boolean: boolean
153+
counter: { value: number }
147154
}>()
148155

149156
const uninjectedState = combinedReducer(undefined, dummyAction())
@@ -189,5 +196,20 @@ describe('combineSlices', () => {
189196
booleanReducer.getInitialState(),
190197
)
191198
})
199+
it('caches initial state', () => {
200+
const beforeInject = combinedReducer(undefined, dummyAction())
201+
const injectedReducer = combinedReducer.inject(counterReducer)
202+
const selectCounter = injectedReducer.selector((state) => state.counter)
203+
const counter = selectCounter(beforeInject)
204+
expect(counter).toBe(selectCounter(beforeInject))
205+
206+
injectedReducer.inject(
207+
{ reducerPath: 'counter', reducer: () => ({ value: 0 }) },
208+
{ overrideExisting: true },
209+
)
210+
const counter2 = selectCounter(beforeInject)
211+
expect(counter2).not.toBe(counter)
212+
expect(counter2).toBe(selectCounter(beforeInject))
213+
})
192214
})
193215
})

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

+32
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,38 @@ describe('createSlice', () => {
644644
// these should be different
645645
expect(injected.selectors).not.toBe(injected2.selectors)
646646
})
647+
it('caches initial states for selectors', () => {
648+
const slice = createSlice({
649+
name: 'counter',
650+
initialState: () => ({ value: 0 }),
651+
reducers: {},
652+
selectors: {
653+
selectObj: (state) => state,
654+
},
655+
})
656+
// not cached
657+
expect(slice.getInitialState()).not.toBe(slice.getInitialState())
658+
expect(slice.reducer(undefined, { type: 'dummy' })).not.toBe(
659+
slice.reducer(undefined, { type: 'dummy' }),
660+
)
661+
662+
const combinedReducer = combineSlices({
663+
static: slice.reducer,
664+
}).withLazyLoadedSlices<WithSlice<typeof slice>>()
665+
666+
const injected = slice.injectInto(combinedReducer)
667+
668+
// still not cached
669+
expect(injected.getInitialState()).not.toBe(injected.getInitialState())
670+
expect(injected.reducer(undefined, { type: 'dummy' })).not.toBe(
671+
injected.reducer(undefined, { type: 'dummy' }),
672+
)
673+
// cached
674+
expect(injected.selectSlice({})).toBe(injected.selectSlice({}))
675+
expect(injected.selectors.selectObj({})).toBe(
676+
injected.selectors.selectObj({}),
677+
)
678+
})
647679
})
648680
describe('reducers definition with asyncThunks', () => {
649681
it('is disabled by default', () => {

0 commit comments

Comments
 (0)