Skip to content

Commit bce3371

Browse files
committed
emplace things
1 parent 0dc7fdd commit bce3371

File tree

4 files changed

+136
-65
lines changed

4 files changed

+136
-65
lines changed

packages/toolkit/src/combineSlices.ts

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
UnionToIntersection,
99
WithOptionalProp,
1010
} from './tsHelpers'
11+
import { emplace } from './utils'
1112

1213
type SliceLike<ReducerPath extends string, State> = {
1314
reducerPath: ReducerPath
@@ -330,37 +331,34 @@ const stateProxyMap = new WeakMap<object, object>()
330331
const createStateProxy = <State extends object>(
331332
state: State,
332333
reducerMap: Partial<Record<string, Reducer>>
333-
) => {
334-
let proxy = stateProxyMap.get(state)
335-
if (!proxy) {
336-
proxy = new Proxy(state, {
337-
get: (target, prop, receiver) => {
338-
if (prop === ORIGINAL_STATE) return target
339-
const result = Reflect.get(target, prop, receiver)
340-
if (typeof result === 'undefined') {
341-
const reducer = reducerMap[prop.toString()]
342-
if (reducer) {
343-
// ensure action type is random, to prevent reducer treating it differently
344-
const reducerResult = reducer(undefined, { type: nanoid() })
345-
if (typeof reducerResult === 'undefined') {
346-
throw new Error(
347-
`The slice reducer for key "${prop.toString()}" returned undefined when called for selector(). ` +
348-
`If the state passed to the reducer is undefined, you must ` +
349-
`explicitly return the initial state. The initial state may ` +
350-
`not be undefined. If you don't want to set a value for this reducer, ` +
351-
`you can use null instead of undefined.`
352-
)
334+
) =>
335+
emplace(stateProxyMap, state, {
336+
insert: () =>
337+
new Proxy(state, {
338+
get: (target, prop, receiver) => {
339+
if (prop === ORIGINAL_STATE) return target
340+
const result = Reflect.get(target, prop, receiver)
341+
if (typeof result === 'undefined') {
342+
const reducer = reducerMap[prop.toString()]
343+
if (reducer) {
344+
// ensure action type is random, to prevent reducer treating it differently
345+
const reducerResult = reducer(undefined, { type: nanoid() })
346+
if (typeof reducerResult === 'undefined') {
347+
throw new Error(
348+
`The slice reducer for key "${prop.toString()}" returned undefined when called for selector(). ` +
349+
`If the state passed to the reducer is undefined, you must ` +
350+
`explicitly return the initial state. The initial state may ` +
351+
`not be undefined. If you don't want to set a value for this reducer, ` +
352+
`you can use null instead of undefined.`
353+
)
354+
}
355+
return reducerResult
353356
}
354-
return reducerResult
355357
}
356-
}
357-
return result
358-
},
359-
})
360-
stateProxyMap.set(state, proxy)
361-
}
362-
return proxy as State
363-
}
358+
return result
359+
},
360+
}),
361+
}) as State
364362

365363
const original = (state: any) => {
366364
if (!isStateProxy(state)) {

packages/toolkit/src/createSlice.ts

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Action, UnknownAction, Reducer } from 'redux'
2+
import type { Selector } from 'reselect'
23
import type {
34
ActionCreatorWithoutPayload,
45
PayloadAction,
@@ -25,6 +26,7 @@ import type {
2526
OverrideThunkApiConfigs,
2627
} from './createAsyncThunk'
2728
import { createAsyncThunk as _createAsyncThunk } from './createAsyncThunk'
29+
import { emplace } from './utils'
2830

2931
const asyncThunkSymbol = Symbol.for('rtk-slice-createasyncthunk')
3032
// type is annotated because it's too long to infer
@@ -756,35 +758,34 @@ export function buildCreateSlice({ creators }: BuildCreateSliceConfig = {}) {
756758
return _reducer.getInitialState()
757759
},
758760
getSelectors(selectState: (rootState: any) => State = selectSelf) {
759-
let selectorCache = injectedSelectorCache.get(this)
760-
if (!selectorCache) {
761-
selectorCache = new WeakMap()
762-
injectedSelectorCache.set(this, selectorCache)
763-
}
764-
let cached = selectorCache.get(selectState)
765-
if (!cached) {
766-
cached = {}
767-
for (const [name, selector] of Object.entries(
768-
options.selectors ?? {}
769-
)) {
770-
cached[name] = (rootState: any, ...args: any[]) => {
771-
let sliceState = selectState.call(this, rootState)
772-
if (typeof sliceState === 'undefined') {
773-
// check if injectInto has been called
774-
if (this !== slice) {
775-
sliceState = this.getInitialState()
776-
} else if (process.env.NODE_ENV !== 'production') {
777-
throw new Error(
778-
'selectState returned undefined for an uninjected slice reducer'
779-
)
761+
const selectorCache = emplace(injectedSelectorCache, this, {
762+
insert: () => new WeakMap(),
763+
})
764+
765+
return emplace(selectorCache, selectState, {
766+
insert: () => {
767+
const map: Record<string, Selector<any, any>> = {}
768+
for (const [name, selector] of Object.entries(
769+
options.selectors ?? {}
770+
)) {
771+
map[name] = (rootState: any, ...args: any[]) => {
772+
let sliceState = selectState.call(this, rootState)
773+
if (typeof sliceState === 'undefined') {
774+
// check if injectInto has been called
775+
if (this !== slice) {
776+
sliceState = this.getInitialState()
777+
} else if (process.env.NODE_ENV !== 'production') {
778+
throw new Error(
779+
'selectState returned undefined for an uninjected slice reducer'
780+
)
781+
}
780782
}
783+
return selector(sliceState, ...args)
781784
}
782-
return selector(sliceState, ...args)
783785
}
784-
}
785-
selectorCache.set(selectState, cached)
786-
}
787-
return cached as any
786+
return map
787+
},
788+
}) as any
788789
},
789790
selectSlice(state) {
790791
let sliceState = state[this.reducerPath]

packages/toolkit/src/dynamicMiddleware/index.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { compose } from 'redux'
77
import { createAction, isAction } from '../createAction'
88
import { isAllOf } from '../matchers'
99
import { nanoid } from '../nanoid'
10-
import { find } from '../utils'
10+
import { emplace, find } from '../utils'
1111
import type {
1212
WithMiddleware,
1313
AddMiddleware,
@@ -69,15 +69,8 @@ export const createDynamicMiddleware = <
6969
) as AddMiddleware<State, Dispatch>
7070

7171
const getFinalMiddleware: Middleware<{}, State, Dispatch> = (api) => {
72-
const appliedMiddleware = Array.from(middlewareMap.values()).map(
73-
(entry) => {
74-
let applied = entry.applied.get(api)
75-
if (!applied) {
76-
applied = entry.middleware(api)
77-
entry.applied.set(api, applied)
78-
}
79-
return applied
80-
}
72+
const appliedMiddleware = Array.from(middlewareMap.values()).map((entry) =>
73+
emplace(entry.applied, api, { insert: () => entry.middleware(api) })
8174
)
8275
return compose(...appliedMiddleware)
8376
}

packages/toolkit/src/utils.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,82 @@ export class Tuple<Items extends ReadonlyArray<unknown> = []> extends Array<
8787
export function freezeDraftable<T>(val: T) {
8888
return isDraftable(val) ? createNextState(val, () => {}) : val
8989
}
90+
91+
interface WeakMapEmplaceHandler<K extends object, V> {
92+
/**
93+
* Will be called to get value, if no value is currently in map.
94+
*/
95+
insert?(key: K, map: WeakMap<K, V>): V
96+
/**
97+
* Will be called to update a value, if one exists already.
98+
*/
99+
update?(previous: V, key: K, map: WeakMap<K, V>): V
100+
}
101+
102+
interface MapEmplaceHandler<K, V> {
103+
/**
104+
* Will be called to get value, if no value is currently in map.
105+
*/
106+
insert?(key: K, map: Map<K, V>): V
107+
/**
108+
* Will be called to update a value, if one exists already.
109+
*/
110+
update?(previous: V, key: K, map: Map<K, V>): V
111+
}
112+
113+
export function emplace<K, V>(
114+
map: Map<K, V>,
115+
key: K,
116+
handler: MapEmplaceHandler<K, V>
117+
): V
118+
export function emplace<K extends object, V>(
119+
map: WeakMap<K, V>,
120+
key: K,
121+
handler: WeakMapEmplaceHandler<K, V>
122+
): V
123+
/**
124+
* Allow inserting a new value, or updating an existing one
125+
* @throws if called for a key with no current value and no `insert` handler is provided
126+
* @returns current value in map (after insertion/updating)
127+
* ```ts
128+
* // return current value if already in map, otherwise initialise to 0 and return that
129+
* const num = emplace(map, key, {
130+
* insert: () => 0
131+
* })
132+
*
133+
* // increase current value by one if already in map, otherwise initialise to 0
134+
* const num = emplace(map, key, {
135+
* update: (n) => n + 1,
136+
* insert: () => 0,
137+
* })
138+
*
139+
* // only update if value's already in the map - and increase it by one
140+
* if (map.has(key)) {
141+
* const num = emplace(map, key, {
142+
* update: (n) => n + 1,
143+
* })
144+
* }
145+
* ```
146+
*
147+
* @remarks
148+
* Based on https://github.com/tc39/proposal-upsert currently in Stage 2 - maybe in a few years we'll be able to replace this with direct method calls
149+
*/
150+
export function emplace<K extends object, V>(
151+
map: WeakMap<K, V>,
152+
key: K,
153+
handler: WeakMapEmplaceHandler<K, V>
154+
): V {
155+
if (map.has(key)) {
156+
let value = map.get(key) as V
157+
if (handler.update) {
158+
value = handler.update(value, key, map)
159+
map.set(key, value)
160+
}
161+
return value
162+
}
163+
if (!handler.insert)
164+
throw new Error('No insert provided for key not already in map')
165+
const inserted = handler.insert(key, map)
166+
map.set(key, inserted)
167+
return inserted
168+
}

0 commit comments

Comments
 (0)