Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 5 additions & 5 deletions packages/toolkit/src/combineSlices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,8 @@ export type InjectConfig = {
export interface CombinedSliceReducer<
InitialState,
DeclaredState extends InitialState = InitialState,
PreloadedState extends Partial<
Record<keyof PreloadedState, any>
> = Partial<DeclaredState>,
PreloadedState extends Partial<Record<keyof PreloadedState, any>> =
Partial<DeclaredState>,
> extends Reducer<DeclaredState, UnknownAction, PreloadedState> {
/**
* Provide a type for slices that will be injected lazily.
Expand Down Expand Up @@ -354,9 +353,8 @@ const ORIGINAL_STATE = Symbol.for('rtk-state-proxy-original')

const isStateProxy = (value: any) => !!value && !!value[ORIGINAL_STATE]

const stateProxyMap = new WeakMap<object, object>()

const createStateProxy = <State extends object>(
stateProxyMap: WeakMap<object, object>,
state: State,
reducerMap: Partial<Record<PropertyKey, Reducer>>,
initialStateCache: Record<PropertyKey, unknown>,
Expand Down Expand Up @@ -411,6 +409,7 @@ export function combineSlices<Slices extends Array<AnySliceLike | ReducerMap>>(
Id<InitialState<Slices>>,
Partial<Id<InitialPreloadedState<Slices>>>
> {
const stateProxyMap = new WeakMap<object, object>()
const reducerMap = Object.fromEntries(getReducers(slices))

const getReducer = () =>
Expand Down Expand Up @@ -472,6 +471,7 @@ export function combineSlices<Slices extends Array<AnySliceLike | ReducerMap>>(
return function selector(state: State, ...args: Args) {
return selectorFn(
createStateProxy(
stateProxyMap,
selectState ? selectState(state as any, ...args) : state,
reducerMap,
initialStateCache,
Expand Down
34 changes: 34 additions & 0 deletions packages/toolkit/src/tests/combineSlices.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,4 +212,38 @@ describe('combineSlices', () => {
expect(counter2).toBe(selectCounter(beforeInject))
})
})
it('uses separate proxy caches for separate combineSlices instances', () => {
// Two independent combineSlices() with no initial slices both use the
// module-level noopReducer, which returns the same `emptyObject` reference
// as their initial state. If stateProxyMap were module-level (shared), the
// second instance's selector would get the first instance's proxy and
// return the wrong initial value for its injected reducer.
const xReducer1 = createReducer('from-reducer1', () => {})
const xReducer2 = createReducer('from-reducer2', () => {})

const reducer1 = combineSlices()
const reducer2 = combineSlices()

// Capture the pre-injection state (emptyObject from noopReducer).
// Both empty instances share the same emptyObject reference.
const preInjectionState = reducer1(undefined, dummyAction())

const injected1 = reducer1.inject({
reducerPath: 'x' as const,
reducer: xReducer1,
})
const injected2 = reducer2.inject({
reducerPath: 'x' as const,
reducer: xReducer2,
})

const select1x = injected1.selector((state) => state.x)
const select2x = injected2.selector((state) => state.x)

// preInjectionState lacks 'x', so the selector must fall back to the
// injected reducer's initial state. Each instance should use its own
// reducerMap, not the other instance's.
expect(select1x(preInjectionState)).toBe('from-reducer1')
expect(select2x(preInjectionState)).toBe('from-reducer2')
})
})
Loading